Search Engine: Improved UI/UX
parent
e7acff0696
commit
ea40cb82a9
|
|
@ -8,6 +8,8 @@ import { useSearchAutoComplete, transformSearchQuery, UserMapping } from "../../
|
|||
import SearchAutoComplete from "./SearchAutoComplete";
|
||||
import SearchDatePicker from "./SearchDatePicker";
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
import { SIDEBAR_MEMBERS } from "../../mobx/stores/Layout";
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -201,6 +203,11 @@ const searchOptions: SearchOption[] = [
|
|||
description: "user",
|
||||
tooltip: "Find messages mentioning a user"
|
||||
},
|
||||
{
|
||||
label: "has:",
|
||||
description: "video, image, link, audio, file",
|
||||
tooltip: "Messages with specific attachment types"
|
||||
},
|
||||
{
|
||||
label: "before:",
|
||||
description: "specific date",
|
||||
|
|
@ -225,15 +232,11 @@ const searchOptions: SearchOption[] = [
|
|||
label: "server-wide",
|
||||
description: "Entire server",
|
||||
tooltip: "Search in entire server instead of just this channel"
|
||||
},
|
||||
{
|
||||
label: "has:",
|
||||
description: "video, image, link, audio, file",
|
||||
tooltip: "Messages with specific attachment types"
|
||||
}
|
||||
];
|
||||
|
||||
export function SearchBar() {
|
||||
const layout = useApplicationState().layout;
|
||||
const [query, setQuery] = useState("");
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
|
@ -246,28 +249,72 @@ export function SearchBar() {
|
|||
startDisplay: string;
|
||||
endDisplay: string;
|
||||
} | null>(null);
|
||||
const [showServerWideError, setShowServerWideError] = useState(false);
|
||||
const [showDateRangeError, setShowDateRangeError] = useState(false);
|
||||
const [showMultipleHasError, setShowMultipleHasError] = useState(false);
|
||||
const [showDuplicateFilterError, setShowDuplicateFilterError] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const skipNextFocus = useRef(false);
|
||||
|
||||
// Setup autocomplete
|
||||
const {
|
||||
state: autocompleteState,
|
||||
setState: setAutocompleteState,
|
||||
onKeyUp,
|
||||
onKeyDown: onAutocompleteKeyDown,
|
||||
onKeyDown: originalOnAutocompleteKeyDown,
|
||||
onChange: onAutocompleteChange,
|
||||
onClick: onAutocompleteClick,
|
||||
onClick: originalOnAutocompleteClick,
|
||||
onFocus: onAutocompleteFocus,
|
||||
onBlur: onAutocompleteBlur,
|
||||
} = useSearchAutoComplete(setQuery, userMappings, setUserMappings);
|
||||
|
||||
// Wrap the autocomplete click to show options menu after selection
|
||||
const onAutocompleteClick = (userId: string, username: string) => {
|
||||
originalOnAutocompleteClick(userId, username);
|
||||
// Show options menu after user selection
|
||||
setTimeout(() => {
|
||||
setShowOptions(true);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Wrap the onKeyDown handler to show options menu after Enter selection
|
||||
const onAutocompleteKeyDown = (e: KeyboardEvent) => {
|
||||
const handled = originalOnAutocompleteKeyDown(e);
|
||||
// If Enter or Tab was pressed and autocomplete was active, show options menu
|
||||
if (handled && (e.key === "Enter" || e.key === "Tab") && autocompleteState.type !== "none") {
|
||||
setTimeout(() => {
|
||||
setShowOptions(true);
|
||||
}, 100);
|
||||
}
|
||||
return handled;
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
// Check if we should skip showing options this time
|
||||
if (skipNextFocus.current) {
|
||||
skipNextFocus.current = false;
|
||||
onAutocompleteFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
onAutocompleteFocus();
|
||||
setShowOptions(true);
|
||||
// Don't show options if we're in the middle of typing a filter
|
||||
const hasIncompleteFilter = query.match(/\b(from:|mentions:|has:|before:|after:|during:|between:)\s*$/);
|
||||
if (!hasIncompleteFilter && !showDatePicker && !showAttachmentTypes && autocompleteState.type === "none") {
|
||||
setShowOptions(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
// Show options when clicking on the input, even if already focused
|
||||
if (!showOptions && autocompleteState.type === "none" && !showDatePicker) {
|
||||
// Check if we're currently editing a filter
|
||||
const cursorPos = inputRef.current?.selectionStart || 0;
|
||||
const beforeCursor = query.slice(0, cursorPos);
|
||||
|
||||
// Check if cursor is within a filter
|
||||
const isInFilter = beforeCursor.match(/\b(from:|mentions:|has:|before:|after:|during:|between:)[^\\s]*$/);
|
||||
|
||||
// Show options when clicking on the input, if not editing a filter
|
||||
if (!isInFilter && !showOptions && autocompleteState.type === "none" && !showDatePicker && !showAttachmentTypes) {
|
||||
setShowOptions(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -276,11 +323,12 @@ export function SearchBar() {
|
|||
onAutocompleteBlur();
|
||||
// Delay to allow clicking on options
|
||||
setTimeout(() => {
|
||||
// Check if we have an incomplete filter
|
||||
const hasIncompleteFilter = query.match(/\b(from:|mentions:)\s*$/);
|
||||
// Check if we have an incomplete filter that should keep autocomplete open
|
||||
const hasUserFilter = query.match(/\b(from:|mentions:)[\s\S]*$/);
|
||||
const hasIncompleteDateFilter = query.match(/\b(before:|after:|during:)\s*$/);
|
||||
|
||||
if (!hasIncompleteFilter && !hasIncompleteDateFilter && !showDatePicker) {
|
||||
// Don't close options/autocomplete if we have a user filter
|
||||
if (!hasUserFilter && !hasIncompleteDateFilter && !showDatePicker) {
|
||||
setShowOptions(false);
|
||||
if (autocompleteState.type === "none") {
|
||||
setAutocompleteState({ type: "none" });
|
||||
|
|
@ -304,6 +352,22 @@ export function SearchBar() {
|
|||
|
||||
// Check for filters
|
||||
const beforeCursor = value.slice(0, cursorPos);
|
||||
|
||||
// Check for user filters (from: and mentions:) - these should trigger autocomplete
|
||||
const fromMatch = beforeCursor.match(/\bfrom:[\s\S]*$/);
|
||||
const mentionsMatch = beforeCursor.match(/\bmentions:[\s\S]*$/);
|
||||
|
||||
if (fromMatch || mentionsMatch) {
|
||||
// Close other dropdowns
|
||||
setShowOptions(false);
|
||||
setShowDatePicker(null);
|
||||
setShowAttachmentTypes(false);
|
||||
// Trigger autocomplete immediately for user filters
|
||||
onAutocompleteChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for date filters
|
||||
const beforeMatch = beforeCursor.match(/\bbefore:\s*$/);
|
||||
const afterMatch = beforeCursor.match(/\bafter:\s*$/);
|
||||
const duringMatch = beforeCursor.match(/\bduring:\s*$/);
|
||||
|
|
@ -342,7 +406,7 @@ export function SearchBar() {
|
|||
setShowAttachmentTypes(false);
|
||||
}
|
||||
|
||||
// Only trigger autocomplete if no filter is active
|
||||
// Only trigger autocomplete if no date/has filter is active
|
||||
const filterActive = value.match(/\b(before:|after:|during:|between:|has:)\s*$/);
|
||||
if (!filterActive) {
|
||||
onAutocompleteChange(e);
|
||||
|
|
@ -366,8 +430,47 @@ export function SearchBar() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate filters (only one of each type allowed)
|
||||
const checkDuplicates = (pattern: RegExp, filterName: string): boolean => {
|
||||
const matches = trimmedQuery.match(pattern) || [];
|
||||
if (matches.length > 1) {
|
||||
setShowDuplicateFilterError(true);
|
||||
setTimeout(() => setShowDuplicateFilterError(false), 3000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check each filter type for duplicates
|
||||
if (checkDuplicates(/\bfrom:@?[\w-]+/gi, "from")) return;
|
||||
if (checkDuplicates(/\bmentions:@?[\w-]+/gi, "mentions")) return;
|
||||
if (checkDuplicates(/\bbefore:\d{4}-\d{2}-\d{2}/gi, "before")) return;
|
||||
if (checkDuplicates(/\bafter:\d{4}-\d{2}-\d{2}/gi, "after")) return;
|
||||
if (checkDuplicates(/\bduring:\d{4}-\d{2}-\d{2}/gi, "during")) return;
|
||||
if (checkDuplicates(/\bbetween:\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}/gi, "between")) return;
|
||||
|
||||
// Check for multiple has: filters (only one attachment type filter allowed)
|
||||
const hasFilterMatches = trimmedQuery.match(/\bhas:(video|image|link|audio|file)/gi) || [];
|
||||
if (hasFilterMatches.length > 1) {
|
||||
// Show tooltip error message - only one attachment type filter is allowed
|
||||
setShowMultipleHasError(true);
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => setShowMultipleHasError(false), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for multiple date-range occurrences
|
||||
const dateRangeCount = (trimmedQuery.match(/\bdate-range\b/g) || []).length;
|
||||
if (dateRangeCount > 1) {
|
||||
// Show tooltip error message - only one date range is allowed
|
||||
setShowDateRangeError(true);
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => setShowDateRangeError(false), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have "date-range" in the query and replace it with actual dates
|
||||
if (trimmedQuery.includes("date-range") && activeDateRange) {
|
||||
if (dateRangeCount === 1 && activeDateRange) {
|
||||
// Replace "date-range" with the actual between filter for processing
|
||||
trimmedQuery = trimmedQuery.replace(/\bdate-range\b/, `between:${activeDateRange.start}..${activeDateRange.end}`);
|
||||
}
|
||||
|
|
@ -375,6 +478,22 @@ export function SearchBar() {
|
|||
// Transform query to use user IDs
|
||||
const searchParams = transformSearchQuery(trimmedQuery, userMappings);
|
||||
|
||||
// Check if only server-wide is present without other filters
|
||||
if (searchParams.server_wide &&
|
||||
!searchParams.query &&
|
||||
!searchParams.author &&
|
||||
!searchParams.mention &&
|
||||
!searchParams.date_start &&
|
||||
!searchParams.date_end &&
|
||||
!searchParams.has) {
|
||||
|
||||
// Show tooltip error message - server-wide requires other filters
|
||||
setShowServerWideError(true);
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => setShowServerWideError(false), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have any search criteria (query text or filters)
|
||||
// Allow empty query string if filters are present
|
||||
const hasFilters = searchParams.author ||
|
||||
|
|
@ -387,8 +506,18 @@ export function SearchBar() {
|
|||
const hasSearchCriteria = (searchParams.query && searchParams.query.trim() !== "") || hasFilters;
|
||||
|
||||
if (hasSearchCriteria) {
|
||||
// Open search in right sidebar with transformed query
|
||||
internalEmit("RightSidebar", "open", "search", searchParams);
|
||||
// Ensure the sidebar container is visible for desktop users
|
||||
if (!isTouchscreenDevice && !layout.getSectionState(SIDEBAR_MEMBERS, true)) {
|
||||
layout.setSectionState(SIDEBAR_MEMBERS, true, true);
|
||||
// Small delay to ensure the sidebar container is rendered first
|
||||
setTimeout(() => {
|
||||
internalEmit("RightSidebar", "open", "search", searchParams);
|
||||
}, 50);
|
||||
} else {
|
||||
// Sidebar is already visible, emit immediately
|
||||
internalEmit("RightSidebar", "open", "search", searchParams);
|
||||
}
|
||||
|
||||
setShowOptions(false);
|
||||
setIsSearching(true);
|
||||
setAutocompleteState({ type: "none" });
|
||||
|
|
@ -700,12 +829,16 @@ export function SearchBar() {
|
|||
if (option.label === "before:" || option.label === "after:" || option.label === "during:" || option.label === "between:") {
|
||||
setShowDatePicker(option.label.slice(0, -1) as "before" | "after" | "during" | "between");
|
||||
setShowOptions(false);
|
||||
// Ensure autocomplete is hidden
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else if (option.label === "has:") {
|
||||
// For has: filter, add it and show attachment type options
|
||||
const newQuery = query + (query ? " " : "") + "has:";
|
||||
setQuery(newQuery);
|
||||
setShowOptions(false);
|
||||
setShowAttachmentTypes(true);
|
||||
// Ensure autocomplete is hidden
|
||||
setAutocompleteState({ type: "none" });
|
||||
|
||||
// Move cursor after "has:"
|
||||
setTimeout(() => {
|
||||
|
|
@ -720,6 +853,11 @@ export function SearchBar() {
|
|||
const newQuery = query + (query ? " " : "") + "server-wide ";
|
||||
setQuery(newQuery);
|
||||
setShowOptions(false);
|
||||
// Ensure autocomplete is hidden
|
||||
setAutocompleteState({ type: "none" });
|
||||
|
||||
// Set flag to skip showing options on the next focus
|
||||
skipNextFocus.current = true;
|
||||
|
||||
// Move cursor to end after the space
|
||||
setTimeout(() => {
|
||||
|
|
@ -729,10 +867,26 @@ export function SearchBar() {
|
|||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
} else if (option.label === "from:" || option.label === "mentions:") {
|
||||
// For user filters, add the text and let the user type to trigger autocomplete
|
||||
const newQuery = query + (query ? " " : "") + option.label;
|
||||
setQuery(newQuery);
|
||||
setShowOptions(false);
|
||||
|
||||
// Focus and position cursor
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
const endPos = newQuery.length;
|
||||
inputRef.current.setSelectionRange(endPos, endPos);
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
// For other filters, add the text immediately
|
||||
const newQuery = query + (query ? " " : "") + option.label;
|
||||
setQuery(newQuery);
|
||||
// Ensure autocomplete is hidden
|
||||
setAutocompleteState({ type: "none" });
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
|
@ -854,15 +1008,31 @@ export function SearchBar() {
|
|||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
{isSearching ? (
|
||||
<IconButton onClick={handleClear}>
|
||||
<X size={18} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={handleSearch}>
|
||||
<Search size={18} />
|
||||
</IconButton>
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
showServerWideError
|
||||
? "Server-wide search requires at least one other filter or search term"
|
||||
: showDateRangeError
|
||||
? "Only one date range filter is allowed"
|
||||
: showMultipleHasError
|
||||
? "Only one attachment type filter is allowed"
|
||||
: showDuplicateFilterError
|
||||
? "Only one of each filter type is allowed"
|
||||
: ""
|
||||
}
|
||||
visible={!isSearching && (showServerWideError || showDateRangeError || showMultipleHasError || showDuplicateFilterError)}
|
||||
placement="bottom"
|
||||
>
|
||||
{isSearching ? (
|
||||
<IconButton onClick={handleClear}>
|
||||
<X size={18} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={handleSearch}>
|
||||
<Search size={18} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
{autocompleteState.type !== "none" && (
|
||||
<SearchAutoComplete
|
||||
state={autocompleteState}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ type SearchState =
|
|||
}
|
||||
| {
|
||||
type: "results";
|
||||
results: MessageI[];
|
||||
pages: Map<number, MessageI[]>;
|
||||
currentPage: number;
|
||||
hasMore: boolean;
|
||||
isLoadingPage?: boolean;
|
||||
};
|
||||
|
||||
// Custom wider sidebar for search results
|
||||
|
|
@ -128,19 +131,48 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
|
||||
const [state, setState] = useState<SearchState>({ type: "waiting" });
|
||||
const [savedSearchParams, setSavedSearchParams] = useState(searchParams);
|
||||
const [pageLastMessageIds, setPageLastMessageIds] = useState<Map<number, string>>(new Map());
|
||||
|
||||
const MESSAGES_PER_PAGE = 50;
|
||||
|
||||
async function search() {
|
||||
async function searchPage(pageNumber: number) {
|
||||
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" });
|
||||
// Check if we already have this page cached
|
||||
if (state.type === "results" && state.pages.has(pageNumber) && pageNumber !== state.currentPage) {
|
||||
setState({ ...state, currentPage: pageNumber });
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark as loading page
|
||||
if (state.type === "results") {
|
||||
setState({ ...state, isLoadingPage: true });
|
||||
} else {
|
||||
setState({ type: "loading" });
|
||||
}
|
||||
|
||||
const searchOptions: any = {
|
||||
query: searchQuery,
|
||||
sort
|
||||
sort,
|
||||
limit: MESSAGES_PER_PAGE
|
||||
};
|
||||
|
||||
// Add pagination cursor for pages after the first
|
||||
if (pageNumber > 1) {
|
||||
const previousPageLastId = pageLastMessageIds.get(pageNumber - 1);
|
||||
if (previousPageLastId) {
|
||||
searchOptions.before = previousPageLastId;
|
||||
} else {
|
||||
// If we don't have the previous page, we need to load pages sequentially
|
||||
// This shouldn't happen in normal navigation
|
||||
console.warn("Previous page not loaded, loading sequentially");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add user filters if provided
|
||||
if (searchParams?.author) {
|
||||
searchOptions.author = searchParams.author;
|
||||
|
|
@ -168,17 +200,72 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
}
|
||||
|
||||
const data = await channel.searchWithUsers(searchOptions);
|
||||
setState({ type: "results", results: data.messages });
|
||||
|
||||
// Store the last message ID for this page
|
||||
if (data.messages.length > 0) {
|
||||
const newPageLastIds = new Map(pageLastMessageIds);
|
||||
newPageLastIds.set(pageNumber, data.messages[data.messages.length - 1]._id);
|
||||
setPageLastMessageIds(newPageLastIds);
|
||||
}
|
||||
|
||||
if (state.type === "results") {
|
||||
// Add this page to the cache
|
||||
const newPages = new Map(state.pages);
|
||||
newPages.set(pageNumber, data.messages);
|
||||
setState({
|
||||
type: "results",
|
||||
pages: newPages,
|
||||
currentPage: pageNumber,
|
||||
hasMore: data.messages.length === MESSAGES_PER_PAGE,
|
||||
isLoadingPage: false
|
||||
});
|
||||
} else {
|
||||
// First page load
|
||||
const newPages = new Map<number, MessageI[]>();
|
||||
newPages.set(1, data.messages);
|
||||
setState({
|
||||
type: "results",
|
||||
pages: newPages,
|
||||
currentPage: 1,
|
||||
hasMore: data.messages.length === MESSAGES_PER_PAGE,
|
||||
isLoadingPage: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
if (state.type === "results" && state.hasMore && !state.isLoadingPage) {
|
||||
searchPage(state.currentPage + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
if (state.type === "results" && state.currentPage > 1 && !state.isLoadingPage) {
|
||||
searchPage(state.currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
search();
|
||||
// Reset to page 1 when search params change
|
||||
searchPage(1);
|
||||
// Clear cached pages when search params change
|
||||
setPageLastMessageIds(new Map());
|
||||
// Save search params when they change
|
||||
if (searchParams) {
|
||||
setSavedSearchParams(searchParams);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [sort, query, searchParams]);
|
||||
}, [
|
||||
sort,
|
||||
query,
|
||||
searchParams?.query,
|
||||
searchParams?.author,
|
||||
searchParams?.mention,
|
||||
searchParams?.date_start,
|
||||
searchParams?.date_end,
|
||||
searchParams?.has,
|
||||
searchParams?.server_wide
|
||||
]);
|
||||
|
||||
return (
|
||||
<SearchSidebarBase>
|
||||
|
|
@ -201,38 +288,31 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
))}
|
||||
</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";
|
||||
{state.type === "results" && (() => {
|
||||
const currentPageMessages = state.pages.get(state.currentPage) || [];
|
||||
return (
|
||||
<>
|
||||
<Overline type="subtle" block style={{ textAlign: 'center', marginTop: '12px' }}>
|
||||
{currentPageMessages.length > 0
|
||||
? currentPageMessages.length === 1 ? 'Result' : 'Results'
|
||||
: 'No Results'
|
||||
}
|
||||
</Overline>
|
||||
|
||||
return (
|
||||
<div key={channelId}>
|
||||
<Overline type="subtle" block>
|
||||
# {channelName}
|
||||
</Overline>
|
||||
{messages.map((message) => {
|
||||
{state.isLoadingPage ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
<Preloader type="ring" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="list">
|
||||
{currentPageMessages.map((message, index) => {
|
||||
const messageChannel = client.channels.get(message.channel_id);
|
||||
const channelName = messageChannel?.name || "Unknown Channel";
|
||||
|
||||
// Check if this is the first message or if the channel changed from the previous message
|
||||
const showChannelIndicator = index === 0 ||
|
||||
message.channel_id !== currentPageMessages[index - 1].channel_id;
|
||||
|
||||
let href = "";
|
||||
if (messageChannel?.channel_type === "TextChannel") {
|
||||
href += `/server/${messageChannel.server_id}`;
|
||||
|
|
@ -240,35 +320,76 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
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 key={message._id}>
|
||||
{showChannelIndicator && (
|
||||
<Overline type="subtle" block>
|
||||
# {channelName}
|
||||
</Overline>
|
||||
)}
|
||||
<div
|
||||
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>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Navigation with page count at the bottom - only show if there are results */}
|
||||
{currentPageMessages.length > 0 && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
justifyContent: 'center',
|
||||
margin: '16px 0 8px 0'
|
||||
}}>
|
||||
<Button
|
||||
compact
|
||||
palette="secondary"
|
||||
disabled={state.currentPage === 1 || state.isLoadingPage}
|
||||
onClick={goToPreviousPage}
|
||||
>
|
||||
← Back
|
||||
</Button>
|
||||
|
||||
<span style={{
|
||||
color: 'var(--tertiary-foreground)',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
Page {state.currentPage}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
compact
|
||||
palette="secondary"
|
||||
disabled={!state.hasMore || state.isLoadingPage}
|
||||
onClick={goToNextPage}
|
||||
>
|
||||
Next →
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</SearchBase>
|
||||
</GenericSidebarList>
|
||||
</SearchSidebarBase>
|
||||
|
|
|
|||
Loading…
Reference in New Issue