ui: message improvements
- Message input: improve focus - Server home icon: improve sizing and padding relative to other borders on page - Chat page: improve loading more messages through use effect scroll
This commit is contained in:
@@ -51,21 +51,21 @@ const ServerSidebar: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="sidebar-primary flex flex-col items-center h-full py-2 space-y-2">
|
<div className="sidebar-primary flex flex-col items-center h-full space-y-2">
|
||||||
{/* Home/DM Button */}
|
{/* Home/DM Button */}
|
||||||
<Tooltip>
|
<Tooltip key={"home-server"}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className={`w-12 h-12 ml-2 rounded-2xl hover:rounded-xl transition-all duration-200 ${
|
className={`w-10 h-10 p-0 mt-2 ml-2 rounded-full transition-all duration-200 ${
|
||||||
!instanceId || instanceId === "@me"
|
!instanceId || instanceId === "@me"
|
||||||
? "bg-primary text-primary-foreground rounded-xl"
|
? "bg-primary text-primary-foreground"
|
||||||
: "hover:bg-primary/10"
|
: "hover:bg-primary/10"
|
||||||
}`}
|
}`}
|
||||||
onClick={handleHomeClick}
|
onClick={handleHomeClick}
|
||||||
>
|
>
|
||||||
<Home size={24} />
|
<Home size={4} />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right">
|
<TooltipContent side="right">
|
||||||
@@ -74,7 +74,7 @@ const ServerSidebar: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="w-12 ml-2 h-0.5 bg-border rounded-full" />
|
<div className="w-full ml-0 h-0.5 bg-border rounded-full" />
|
||||||
|
|
||||||
{/* Server List */}
|
{/* Server List */}
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
|
<div className="flex-1 flex flex-col overflow-y-auto scrollbar-thin scrollbar-thumb-border space-y-2">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface MessageInputProps {
|
|||||||
replyingTo?: Message | null;
|
replyingTo?: Message | null;
|
||||||
onCancelReply?: () => void;
|
onCancelReply?: () => void;
|
||||||
replyingToUser: MessageUser | null;
|
replyingToUser: MessageUser | null;
|
||||||
|
messageInputRef: React.RefObject<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageInput: React.FC<MessageInputProps> = ({
|
export const MessageInput: React.FC<MessageInputProps> = ({
|
||||||
@@ -23,9 +24,10 @@ export const MessageInput: React.FC<MessageInputProps> = ({
|
|||||||
replyingTo,
|
replyingTo,
|
||||||
onCancelReply,
|
onCancelReply,
|
||||||
replyingToUser,
|
replyingToUser,
|
||||||
|
messageInputRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = messageInputRef;
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
// Use the API hook for sending messages
|
// Use the API hook for sending messages
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { MessageInput } from "@/components/message/MessageInput";
|
|||||||
const ChatPage: React.FC = () => {
|
const ChatPage: React.FC = () => {
|
||||||
const { instanceId, channelId } = useParams();
|
const { instanceId, channelId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const messageInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: instance,
|
data: instance,
|
||||||
@@ -39,6 +40,7 @@ const ChatPage: React.FC = () => {
|
|||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const messagesStartRef = useRef<HTMLDivElement>(null);
|
const messagesStartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null); // Ref for the ScrollArea content wrapper
|
||||||
|
|
||||||
// API mutation hooks - called unconditionally
|
// API mutation hooks - called unconditionally
|
||||||
const loadMoreMessagesMutation = useLoadMoreMessages(channelId);
|
const loadMoreMessagesMutation = useLoadMoreMessages(channelId);
|
||||||
@@ -71,6 +73,7 @@ const ChatPage: React.FC = () => {
|
|||||||
|
|
||||||
// Effects - called unconditionally
|
// Effects - called unconditionally
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Scroll to bottom when messages load or update
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [channelMessages]);
|
}, [channelMessages]);
|
||||||
|
|
||||||
@@ -102,6 +105,40 @@ const ChatPage: React.FC = () => {
|
|||||||
[channelMessages],
|
[channelMessages],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Focus the message input
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageInputRef.current) {
|
||||||
|
messageInputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [handleReply]);
|
||||||
|
|
||||||
|
// Effect for scroll to top and load more
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollAreaElement = scrollAreaRef.current;
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (
|
||||||
|
scrollAreaElement &&
|
||||||
|
scrollAreaElement.scrollTop === 0 &&
|
||||||
|
channelMessages &&
|
||||||
|
channelMessages.length > 0 &&
|
||||||
|
!isLoadingMore
|
||||||
|
) {
|
||||||
|
handleLoadMore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (scrollAreaElement) {
|
||||||
|
scrollAreaElement.addEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (scrollAreaElement) {
|
||||||
|
scrollAreaElement.removeEventListener("scroll", handleScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [channelMessages, isLoadingMore, handleLoadMore]);
|
||||||
|
|
||||||
// Handle loading states
|
// Handle loading states
|
||||||
if (instanceLoading || messagesLoading || usersLoading) {
|
if (instanceLoading || messagesLoading || usersLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -223,84 +260,62 @@ const ChatPage: React.FC = () => {
|
|||||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||||
{/* Messages Area */}
|
{/* Messages Area */}
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
{/* Load More Button */}
|
{/* Attach ref to the actual scrollable content */}
|
||||||
{channelMessages && channelMessages.length > 0 && (
|
<div ref={scrollAreaRef} className="h-full overflow-y-auto">
|
||||||
<div className="flex justify-center py-2">
|
<div ref={messagesStartRef} />
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleLoadMore}
|
|
||||||
disabled={isLoadingMore}
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{isLoadingMore ? (
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
|
|
||||||
) : (
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{isLoadingMore ? "Loading..." : "Load older messages"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div ref={messagesStartRef} />
|
{/* Welcome Message */}
|
||||||
|
<div className="px-4 py-6 border-b border-concord/50 flex-shrink-0">
|
||||||
{/* Welcome Message */}
|
<div className="flex items-center space-x-3 mb-3">
|
||||||
<div className="px-4 py-6 border-b border-concord/50 flex-shrink-0">
|
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center">
|
||||||
<div className="flex items-center space-x-3 mb-3">
|
<ChannelIcon size={24} className="text-primary-foreground" />
|
||||||
<div className="w-16 h-16 bg-primary rounded-full flex items-center justify-center">
|
</div>
|
||||||
<ChannelIcon size={24} className="text-primary-foreground" />
|
<div>
|
||||||
</div>
|
<h3 className="text-2xl font-bold text-concord-primary">
|
||||||
<div>
|
Welcome to #{currentChannel?.name}!
|
||||||
<h3 className="text-2xl font-bold text-concord-primary">
|
</h3>
|
||||||
Welcome to #{currentChannel?.name}!
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{currentChannel?.description && (
|
|
||||||
<div className="text-concord-secondary bg-concord-secondary/50 p-3 rounded border-l-4 border-primary">
|
|
||||||
{currentChannel.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pb-4">
|
|
||||||
{/* Messages */}
|
|
||||||
{sortedMessages && sortedMessages.length > 0 ? (
|
|
||||||
<div>
|
|
||||||
{sortedMessages.map((message) => {
|
|
||||||
console.log(message);
|
|
||||||
const user = users?.find((u) => u.id === message.userId);
|
|
||||||
const replyToMessage = channelMessages?.find(
|
|
||||||
(m) => m.id === message.replies?.repliesToId,
|
|
||||||
);
|
|
||||||
const replyToUser = replyToMessage
|
|
||||||
? users?.find((u) => u.id === replyToMessage.userId)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MessageComponent
|
|
||||||
key={message.id}
|
|
||||||
message={message}
|
|
||||||
user={user}
|
|
||||||
replyTo={replyToMessage}
|
|
||||||
onReply={handleReply}
|
|
||||||
replyToUser={replyToUser}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center h-64">
|
|
||||||
<div className="text-center text-concord-secondary">
|
|
||||||
<p>No messages yet. Start the conversation!</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
<div className="pb-4">
|
||||||
|
{/* Messages */}
|
||||||
|
{sortedMessages && sortedMessages.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{sortedMessages.map((message) => {
|
||||||
|
console.log(message);
|
||||||
|
const user = users?.find((u) => u.id === message.userId);
|
||||||
|
const replyToMessage = channelMessages?.find(
|
||||||
|
(m) => m.id === message.replies?.repliesToId,
|
||||||
|
);
|
||||||
|
const replyToUser = replyToMessage
|
||||||
|
? users?.find((u) => u.id === replyToMessage.userId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageComponent
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
user={user}
|
||||||
|
replyTo={replyToMessage}
|
||||||
|
onReply={handleReply}
|
||||||
|
replyToUser={replyToUser}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center text-concord-secondary">
|
||||||
|
<p>No messages yet. Start the conversation!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
@@ -317,6 +332,7 @@ const ChatPage: React.FC = () => {
|
|||||||
? users?.find((u) => u.id === replyingTo.userId) || null
|
? users?.find((u) => u.id === replyingTo.userId) || null
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
messageInputRef={messageInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user