276 lines
10 KiB
TypeScript
276 lines
10 KiB
TypeScript
import { Link, useParams, useHistory } from "react-router-dom";
|
|
import { Message as MessageI } from "revolt.js";
|
|
import styled from "styled-components/macro";
|
|
|
|
import { Text } from "preact-i18n";
|
|
import { useEffect, useState } from "preact/hooks";
|
|
|
|
import { Button, Preloader } from "@revoltchat/ui";
|
|
|
|
import { useClient } from "../../../controllers/client/ClientController";
|
|
import { internalEmit } from "../../../lib/eventEmitter";
|
|
import Message from "../../common/messaging/Message";
|
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
|
|
|
type SearchState =
|
|
| {
|
|
type: "waiting";
|
|
}
|
|
| {
|
|
type: "loading";
|
|
}
|
|
| {
|
|
type: "results";
|
|
results: MessageI[];
|
|
};
|
|
|
|
// Custom wider sidebar for search results
|
|
const SearchSidebarBase = styled(GenericSidebarBase)`
|
|
width: 360px; /* Increased from 232px */
|
|
|
|
@media (max-width: 1200px) {
|
|
width: 320px;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
width: 280px;
|
|
}
|
|
`;
|
|
|
|
const SearchBase = styled.div`
|
|
padding: 6px;
|
|
padding-top: 48px; /* Add space for the header */
|
|
|
|
input {
|
|
width: 100%;
|
|
}
|
|
|
|
.list {
|
|
gap: 4px;
|
|
margin: 8px 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.message {
|
|
margin: 4px 2px 8px 2px;
|
|
padding: 8px;
|
|
overflow: hidden;
|
|
border-radius: var(--border-radius);
|
|
background: var(--primary-background);
|
|
|
|
&:hover {
|
|
background: var(--hover);
|
|
}
|
|
|
|
> * {
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Override message text color but preserve mentions and other highlights */
|
|
p {
|
|
color: var(--foreground) !important;
|
|
}
|
|
|
|
/* Also override any direct text that might be themed */
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.sort {
|
|
gap: 4px;
|
|
margin: 6px 0;
|
|
display: flex;
|
|
|
|
> * {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
}
|
|
`;
|
|
|
|
const Overline = styled.div<{ type?: string; block?: boolean }>`
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: ${props => props.type === "error" ? "var(--error)" : "var(--tertiary-foreground)"};
|
|
margin: 0.4em 0;
|
|
cursor: ${props => props.type === "error" ? "pointer" : "default"};
|
|
display: ${props => props.block ? "block" : "inline"};
|
|
|
|
&:hover {
|
|
${props => props.type === "error" && "text-decoration: underline;"}
|
|
}
|
|
`;
|
|
|
|
interface Props {
|
|
close: () => void;
|
|
initialQuery?: string;
|
|
searchParams?: {
|
|
query: string;
|
|
author?: string;
|
|
mention?: string;
|
|
date_start?: string;
|
|
date_end?: string;
|
|
has?: string;
|
|
server_wide?: boolean;
|
|
};
|
|
}
|
|
|
|
export function SearchSidebar({ close, initialQuery = "", searchParams }: Props) {
|
|
const client = useClient();
|
|
const history = useHistory();
|
|
const params = useParams<{ channel: string }>();
|
|
const channel = client.channels.get(params.channel)!;
|
|
|
|
type Sort = "Relevance" | "Latest" | "Oldest";
|
|
const [sort, setSort] = useState<Sort>("Latest");
|
|
const [query, setQuery] = useState(searchParams?.query || initialQuery);
|
|
|
|
const [state, setState] = useState<SearchState>({ type: "waiting" });
|
|
const [savedSearchParams, setSavedSearchParams] = useState(searchParams);
|
|
|
|
async function search() {
|
|
const searchQuery = searchParams?.query || query;
|
|
if (!searchQuery && !searchParams?.author && !searchParams?.mention &&
|
|
!searchParams?.date_start && !searchParams?.date_end &&
|
|
!searchParams?.has && !searchParams?.server_wide) return;
|
|
|
|
setState({ type: "loading" });
|
|
const searchOptions: any = {
|
|
query: searchQuery,
|
|
sort
|
|
};
|
|
|
|
// Add user filters if provided
|
|
if (searchParams?.author) {
|
|
searchOptions.author = searchParams.author;
|
|
}
|
|
if (searchParams?.mention) {
|
|
searchOptions.mention = searchParams.mention;
|
|
}
|
|
|
|
// Add date filters if provided using the new standardized parameters
|
|
if (searchParams?.date_start) {
|
|
searchOptions.date_start = searchParams.date_start;
|
|
}
|
|
if (searchParams?.date_end) {
|
|
searchOptions.date_end = searchParams.date_end;
|
|
}
|
|
|
|
// Add server-wide filter if provided
|
|
if (searchParams?.server_wide) {
|
|
searchOptions.server_wide = true;
|
|
}
|
|
|
|
// Add has filter if provided
|
|
if (searchParams?.has) {
|
|
searchOptions.has = searchParams.has;
|
|
}
|
|
|
|
const data = await channel.searchWithUsers(searchOptions);
|
|
setState({ type: "results", results: data.messages });
|
|
}
|
|
|
|
useEffect(() => {
|
|
search();
|
|
// Save search params when they change
|
|
if (searchParams) {
|
|
setSavedSearchParams(searchParams);
|
|
}
|
|
// eslint-disable-next-line
|
|
}, [sort, query, searchParams]);
|
|
|
|
return (
|
|
<SearchSidebarBase>
|
|
<GenericSidebarList>
|
|
<SearchBase>
|
|
<Overline type="subtle" block>
|
|
<Text id="app.main.channel.search.title" />
|
|
</Overline>
|
|
<div className="sort">
|
|
{(["Latest", "Oldest", "Relevance"] as Sort[]).map((key) => (
|
|
<Button
|
|
key={key}
|
|
compact
|
|
palette={sort === key ? "accent" : "secondary"}
|
|
onClick={() => setSort(key)}>
|
|
<Text
|
|
id={`app.main.channel.search.sort.${key.toLowerCase()}`}
|
|
/>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
{state.type === "loading" && <Preloader type="ring" />}
|
|
{state.type === "results" && (
|
|
<>
|
|
<Overline type="subtle" block style={{ textAlign: 'center', marginTop: '12px' }}>
|
|
{state.results.length > 0
|
|
? `${state.results.length} Result${state.results.length === 1 ? '' : 's'}`
|
|
: 'No Results'
|
|
}
|
|
</Overline>
|
|
<div className="list">
|
|
{(() => {
|
|
// Group messages by channel
|
|
const groupedMessages = state.results.reduce((acc, message) => {
|
|
const channelId = message.channel_id;
|
|
if (!acc[channelId]) {
|
|
acc[channelId] = [];
|
|
}
|
|
acc[channelId].push(message);
|
|
return acc;
|
|
}, {} as Record<string, MessageI[]>);
|
|
|
|
const client = useClient();
|
|
|
|
return Object.entries(groupedMessages).map(([channelId, messages]) => {
|
|
const messageChannel = client.channels.get(channelId);
|
|
const channelName = messageChannel?.name || "Unknown Channel";
|
|
|
|
return (
|
|
<div key={channelId}>
|
|
<Overline type="subtle" block>
|
|
# {channelName}
|
|
</Overline>
|
|
{messages.map((message) => {
|
|
let href = "";
|
|
if (messageChannel?.channel_type === "TextChannel") {
|
|
href += `/server/${messageChannel.server_id}`;
|
|
}
|
|
href += `/channel/${message.channel_id}/${message._id}`;
|
|
|
|
return (
|
|
<div
|
|
key={message._id}
|
|
className="message"
|
|
style={{ cursor: 'pointer' }}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
// Navigate to the message
|
|
history.push(href);
|
|
// Re-emit the search sidebar with the same params to keep it open
|
|
setTimeout(() => {
|
|
internalEmit("RightSidebar", "open", "search", savedSearchParams || searchParams);
|
|
}, 100);
|
|
}}
|
|
>
|
|
<Message
|
|
message={message}
|
|
head
|
|
hideReply
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
});
|
|
})()}
|
|
</div>
|
|
</>
|
|
)}
|
|
</SearchBase>
|
|
</GenericSidebarList>
|
|
</SearchSidebarBase>
|
|
);
|
|
} |