Search Engine: Attachment filter and simplified date parameter
parent
54e8614dc8
commit
e7acff0696
|
|
@ -1 +1 @@
|
|||
Subproject commit 4f711d531ec396b0b021be0d5a538223a1b0671f
|
||||
Subproject commit 31c167ed78cc9187ecb324eba90f059fafcc28a9
|
||||
|
|
@ -16,6 +16,12 @@ const Base = styled.div`
|
|||
overflow: hidden;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-top: 8px;
|
||||
max-height: 250px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
|
|
@ -43,6 +49,18 @@ const Base = styled.div`
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
gap: 12px;
|
||||
|
||||
/* Add touch feedback for mobile */
|
||||
&:active {
|
||||
background: var(--secondary-background);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -54,6 +72,10 @@ interface Props {
|
|||
|
||||
export default function SearchAutoComplete({ state, setState, onClick }: Props) {
|
||||
if (state.type !== "user") return null;
|
||||
|
||||
// Detect if we're on mobile
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
const iconSize = isMobile ? 24 : 20;
|
||||
|
||||
return (
|
||||
<Base>
|
||||
|
|
@ -82,16 +104,16 @@ export default function SearchAutoComplete({ state, setState, onClick }: Props)
|
|||
e.stopPropagation();
|
||||
onClick(user._id, user.username);
|
||||
}}>
|
||||
<UserIcon size={20} target={user} status={true} />
|
||||
<UserIcon size={iconSize} target={user} status={true} />
|
||||
<span>{user.username}</span>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div style={{
|
||||
padding: "12px",
|
||||
padding: isMobile ? "16px" : "12px",
|
||||
textAlign: "center",
|
||||
color: "var(--tertiary-foreground)",
|
||||
fontSize: "13px"
|
||||
fontSize: isMobile ? "14px" : "13px"
|
||||
}}>
|
||||
No users found
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { internalEmit } from "../../lib/eventEmitter";
|
|||
import { useSearchAutoComplete, transformSearchQuery, UserMapping } from "../../lib/hooks/useSearchAutoComplete";
|
||||
import SearchAutoComplete from "./SearchAutoComplete";
|
||||
import SearchDatePicker from "./SearchDatePicker";
|
||||
import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
|
@ -28,25 +29,42 @@ const Input = styled.input`
|
|||
background: transparent;
|
||||
color: var(--foreground);
|
||||
font-size: 14px;
|
||||
padding: 6px 2px 6px 12px;
|
||||
padding: 6px 2px 6px 8px; /* Reduced left padding for more space */
|
||||
outline: none;
|
||||
min-width: 0; /* Allow input to shrink properly */
|
||||
|
||||
&::placeholder {
|
||||
color: var(--tertiary-foreground);
|
||||
font-size: 13px; /* Slightly smaller placeholder on mobile */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 13px;
|
||||
padding: 6px 2px 6px 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
const IconButton = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px 0 8px;
|
||||
padding: 0 8px; /* Symmetrical padding */
|
||||
color: var(--tertiary-foreground);
|
||||
cursor: pointer;
|
||||
transition: color 0.1s ease;
|
||||
flex-shrink: 0; /* Prevent icon from shrinking */
|
||||
|
||||
&:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0 6px; /* Less padding on mobile */
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const OptionsDropdown = styled.div`
|
||||
|
|
@ -61,6 +79,29 @@ const OptionsDropdown = styled.div`
|
|||
overflow: hidden;
|
||||
padding: 8px;
|
||||
min-width: 300px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 12px;
|
||||
min-width: 320px;
|
||||
max-width: calc(100vw - 20px);
|
||||
right: -10px; /* Adjust position to prevent edge cutoff */
|
||||
max-height: 40vh; /* Limit height when keyboard is up */
|
||||
overflow-y: auto; /* Make it scrollable */
|
||||
|
||||
/* Add scrollbar styles for mobile */
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const OptionsHeader = styled.div`
|
||||
|
|
@ -70,6 +111,11 @@ const OptionsHeader = styled.div`
|
|||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 11px;
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Option = styled.div`
|
||||
|
|
@ -88,6 +134,18 @@ const Option = styled.div`
|
|||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 10px 10px;
|
||||
margin-bottom: 3px;
|
||||
border-radius: 6px;
|
||||
|
||||
/* Add touch feedback for mobile */
|
||||
&:active {
|
||||
background: var(--secondary-background);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const OptionLabel = styled.span`
|
||||
|
|
@ -97,12 +155,21 @@ const OptionLabel = styled.span`
|
|||
font-family: var(--monospace-font), monospace;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 13px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const OptionDesc = styled.span`
|
||||
color: var(--tertiary-foreground);
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HelpIcon = styled(HelpCircle)`
|
||||
|
|
@ -149,10 +216,20 @@ const searchOptions: SearchOption[] = [
|
|||
description: "specific date",
|
||||
tooltip: "Messages after this date"
|
||||
},
|
||||
{
|
||||
label: "between:",
|
||||
description: "date range",
|
||||
tooltip: "Messages between two dates"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -161,7 +238,14 @@ export function SearchBar() {
|
|||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [userMappings, setUserMappings] = useState<UserMapping>({});
|
||||
const [showDatePicker, setShowDatePicker] = useState<"before" | "after" | "during" | null>(null);
|
||||
const [showDatePicker, setShowDatePicker] = useState<"before" | "after" | "during" | "between" | null>(null);
|
||||
const [showAttachmentTypes, setShowAttachmentTypes] = useState(false);
|
||||
const [activeDateRange, setActiveDateRange] = useState<{
|
||||
start: string;
|
||||
end: string;
|
||||
startDisplay: string;
|
||||
endDisplay: string;
|
||||
} | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Setup autocomplete
|
||||
|
|
@ -218,28 +302,49 @@ export function SearchBar() {
|
|||
|
||||
setQuery(value);
|
||||
|
||||
// Check for date filters
|
||||
// Check for filters
|
||||
const beforeCursor = value.slice(0, cursorPos);
|
||||
const beforeMatch = beforeCursor.match(/\bbefore:\s*$/);
|
||||
const afterMatch = beforeCursor.match(/\bafter:\s*$/);
|
||||
const duringMatch = beforeCursor.match(/\bduring:\s*$/);
|
||||
const betweenMatch = beforeCursor.match(/\bbetween:\s*$/);
|
||||
const hasMatch = beforeCursor.match(/\bhas:\s*$/);
|
||||
|
||||
if (beforeMatch) {
|
||||
setShowDatePicker("before");
|
||||
setShowOptions(false);
|
||||
setShowAttachmentTypes(false);
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else if (afterMatch) {
|
||||
setShowDatePicker("after");
|
||||
setShowOptions(false);
|
||||
setShowAttachmentTypes(false);
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else if (duringMatch) {
|
||||
setShowDatePicker("during");
|
||||
setShowOptions(false);
|
||||
setShowAttachmentTypes(false);
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else if (betweenMatch) {
|
||||
setShowDatePicker("between");
|
||||
setShowOptions(false);
|
||||
setShowAttachmentTypes(false);
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else if (hasMatch) {
|
||||
// Show attachment type options
|
||||
setShowAttachmentTypes(true);
|
||||
setShowOptions(false);
|
||||
setShowDatePicker(null);
|
||||
setAutocompleteState({ type: "none" });
|
||||
} else {
|
||||
// Only trigger autocomplete if no date filter is active
|
||||
const dateFilterActive = value.match(/\b(before:|after:|during:)\s*$/);
|
||||
if (!dateFilterActive) {
|
||||
// Check if "has:" was removed and close attachment types dropdown
|
||||
if (showAttachmentTypes && !value.includes("has:")) {
|
||||
setShowAttachmentTypes(false);
|
||||
}
|
||||
|
||||
// Only trigger autocomplete if no filter is active
|
||||
const filterActive = value.match(/\b(before:|after:|during:|between:|has:)\s*$/);
|
||||
if (!filterActive) {
|
||||
onAutocompleteChange(e);
|
||||
if (showDatePicker) {
|
||||
setShowDatePicker(null);
|
||||
|
|
@ -249,28 +354,37 @@ export function SearchBar() {
|
|||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
const trimmedQuery = query.trim();
|
||||
let trimmedQuery = query.trim();
|
||||
|
||||
// Check for incomplete filters (only user filters, not date filters)
|
||||
// Check for incomplete filters
|
||||
const hasIncompleteUserFilter = trimmedQuery.match(/\b(from:|mentions:)\s*$/);
|
||||
const hasIncompleteDateFilter = trimmedQuery.match(/\b(before:|after:|during:)\s*$/);
|
||||
const hasIncompleteDateFilter = trimmedQuery.match(/\b(before:|after:|during:|between:)\s*$/);
|
||||
const hasIncompleteHasFilter = trimmedQuery.match(/\bhas:\s*$/);
|
||||
|
||||
if (hasIncompleteUserFilter || hasIncompleteDateFilter) {
|
||||
if (hasIncompleteUserFilter || hasIncompleteDateFilter || hasIncompleteHasFilter) {
|
||||
// Don't search if there's an incomplete filter
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have "date-range" in the query and replace it with actual dates
|
||||
if (trimmedQuery.includes("date-range") && activeDateRange) {
|
||||
// Replace "date-range" with the actual between filter for processing
|
||||
trimmedQuery = trimmedQuery.replace(/\bdate-range\b/, `between:${activeDateRange.start}..${activeDateRange.end}`);
|
||||
}
|
||||
|
||||
// Transform query to use user IDs
|
||||
const searchParams = transformSearchQuery(trimmedQuery, userMappings);
|
||||
|
||||
// Check if we have any search criteria (query text or filters)
|
||||
const hasSearchCriteria = searchParams.query ||
|
||||
searchParams.author ||
|
||||
searchParams.mention ||
|
||||
searchParams.before_date ||
|
||||
searchParams.after_date ||
|
||||
searchParams.during ||
|
||||
searchParams.server_wide;
|
||||
// Allow empty query string if filters are present
|
||||
const hasFilters = searchParams.author ||
|
||||
searchParams.mention ||
|
||||
searchParams.date_start ||
|
||||
searchParams.date_end ||
|
||||
searchParams.has ||
|
||||
searchParams.server_wide;
|
||||
|
||||
const hasSearchCriteria = (searchParams.query && searchParams.query.trim() !== "") || hasFilters;
|
||||
|
||||
if (hasSearchCriteria) {
|
||||
// Open search in right sidebar with transformed query
|
||||
|
|
@ -279,6 +393,17 @@ export function SearchBar() {
|
|||
setIsSearching(true);
|
||||
setAutocompleteState({ type: "none" });
|
||||
inputRef.current?.blur();
|
||||
|
||||
// On mobile, automatically slide to show search results
|
||||
if (isTouchscreenDevice) {
|
||||
setTimeout(() => {
|
||||
const panels = document.querySelector("#app > div > div > div");
|
||||
panels?.scrollTo({
|
||||
behavior: "smooth",
|
||||
left: panels.clientWidth * 3,
|
||||
});
|
||||
}, 100); // Small delay to ensure sidebar is opened first
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -286,13 +411,173 @@ export function SearchBar() {
|
|||
const currentValue = (e.currentTarget as HTMLInputElement).value;
|
||||
const cursorPos = (e.currentTarget as HTMLInputElement).selectionStart || 0;
|
||||
|
||||
// Handle backspace/delete for server-wide
|
||||
// Handle backspace/delete for date filters and server-wide
|
||||
if (e.key === "Backspace" || e.key === "Delete") {
|
||||
const beforeCursor = currentValue.slice(0, cursorPos);
|
||||
const afterCursor = currentValue.slice(cursorPos);
|
||||
|
||||
// Check if we're at the end of "server-wide" or within it
|
||||
// Check for date filters with backspace
|
||||
if (e.key === "Backspace") {
|
||||
// Handle single date filters (before:, after:, during:)
|
||||
const singleDateMatch = beforeCursor.match(/\b(before|after|during):(\d{4}-\d{2}-\d{2})\s*$/);
|
||||
if (singleDateMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = singleDateMatch.index!;
|
||||
|
||||
// Remove the entire filter and date
|
||||
const newValue = currentValue.slice(0, filterStart) + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
// Position cursor where filter was
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(filterStart, filterStart);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle filter-only patterns (e.g., "before:" without date)
|
||||
const filterOnlyMatch = beforeCursor.match(/\b(before|after|during|between):\s*$/);
|
||||
if (filterOnlyMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = filterOnlyMatch.index!;
|
||||
|
||||
// Remove the entire filter
|
||||
const newValue = currentValue.slice(0, filterStart) + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
// Close date picker if open
|
||||
setShowDatePicker(null);
|
||||
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(filterStart, filterStart);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle between date range filter
|
||||
const betweenMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})\s*$/);
|
||||
if (betweenMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = betweenMatch.index!;
|
||||
const startDate = betweenMatch[1];
|
||||
|
||||
// Remove end date but keep "between:YYYY-MM-DD.."
|
||||
const newValue = currentValue.slice(0, filterStart) + "between:" + startDate + ".." + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
const newCursorPos = filterStart + "between:".length + startDate.length + 2;
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle partial between filter (between:YYYY-MM-DD..)
|
||||
const partialBetweenMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\.\.\s*$/);
|
||||
if (partialBetweenMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = partialBetweenMatch.index!;
|
||||
const startDate = partialBetweenMatch[1];
|
||||
|
||||
// Remove ".." to get "between:YYYY-MM-DD"
|
||||
const newValue = currentValue.slice(0, filterStart) + "between:" + startDate + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
const newCursorPos = filterStart + "between:".length + startDate.length;
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle between with only start date (between:YYYY-MM-DD)
|
||||
const betweenStartOnlyMatch = beforeCursor.match(/\bbetween:(\d{4}-\d{2}-\d{2})\s*$/);
|
||||
if (betweenStartOnlyMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = betweenStartOnlyMatch.index!;
|
||||
|
||||
// Remove date but keep "between:"
|
||||
const newValue = currentValue.slice(0, filterStart) + "between:" + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
const newCursorPos = filterStart + "between:".length;
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
|
||||
// Open the date picker in between mode
|
||||
setShowDatePicker("between");
|
||||
setShowOptions(false);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle date-range keyword removal
|
||||
const dateRangeMatch = beforeCursor.match(/\bdate-range\s*$/);
|
||||
if (dateRangeMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = dateRangeMatch.index!;
|
||||
|
||||
// Remove the keyword and clear the stored date range
|
||||
const newValue = currentValue.slice(0, filterStart) + afterCursor;
|
||||
setQuery(newValue);
|
||||
setActiveDateRange(null);
|
||||
|
||||
// Position cursor where filter was
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(filterStart, filterStart);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle has: filter removal
|
||||
const hasMatch = beforeCursor.match(/\bhas:(video|image|link|audio|file)\s*$/);
|
||||
if (hasMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = hasMatch.index!;
|
||||
|
||||
// Remove the entire filter
|
||||
const newValue = currentValue.slice(0, filterStart) + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
// Position cursor where filter was
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(filterStart, filterStart);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle has: without value
|
||||
const hasOnlyMatch = beforeCursor.match(/\bhas:\s*$/);
|
||||
if (hasOnlyMatch) {
|
||||
e.preventDefault();
|
||||
const filterStart = hasOnlyMatch.index!;
|
||||
|
||||
// Remove the entire filter
|
||||
const newValue = currentValue.slice(0, filterStart) + afterCursor;
|
||||
setQuery(newValue);
|
||||
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(filterStart, filterStart);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Original server-wide handling
|
||||
const serverWideMatch = beforeCursor.match(/\bserver-wide\s*$/);
|
||||
if (serverWideMatch) {
|
||||
e.preventDefault();
|
||||
|
|
@ -307,7 +592,42 @@ export function SearchBar() {
|
|||
return;
|
||||
}
|
||||
} else if (e.key === "Delete") {
|
||||
// Check if cursor is at the beginning of server-wide
|
||||
// Handle date filters with delete key
|
||||
// Check if we're at a filter: position
|
||||
const filterMatch = afterCursor.match(/^(before|after|during|between):(\d{4}-\d{2}-\d{2})?/);
|
||||
if (filterMatch && beforeCursor.match(/\s$|^$/)) {
|
||||
e.preventDefault();
|
||||
const filterType = filterMatch[1];
|
||||
const hasDate = filterMatch[2];
|
||||
|
||||
if (hasDate) {
|
||||
// Remove entire filter and date
|
||||
const newValue = beforeCursor + afterCursor.slice(filterMatch[0].length).trimStart();
|
||||
setQuery(newValue);
|
||||
} else {
|
||||
// Just filter: without date, remove it
|
||||
const newValue = beforeCursor + afterCursor.slice(filterType.length + 1).trimStart();
|
||||
setQuery(newValue);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.setSelectionRange(beforeCursor.length, beforeCursor.length);
|
||||
}
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle has: filter with delete key
|
||||
const hasAfter = afterCursor.match(/^has:(video|image|link|audio|file)?\s*/);
|
||||
if (hasAfter) {
|
||||
e.preventDefault();
|
||||
const newValue = beforeCursor + afterCursor.slice(hasAfter[0].length);
|
||||
setQuery(newValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// Original server-wide handling
|
||||
const serverWideAfter = afterCursor.match(/^server-wide\s*/);
|
||||
if (serverWideAfter) {
|
||||
e.preventDefault();
|
||||
|
|
@ -377,9 +697,24 @@ export function SearchBar() {
|
|||
|
||||
const handleOptionClick = (option: SearchOption) => {
|
||||
// If it's a date filter, just show the date picker without adding text
|
||||
if (option.label === "before:" || option.label === "after:" || option.label === "during:") {
|
||||
setShowDatePicker(option.label.slice(0, -1) as "before" | "after" | "during");
|
||||
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);
|
||||
} 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);
|
||||
|
||||
// Move cursor after "has:"
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
const endPos = newQuery.length;
|
||||
inputRef.current.setSelectionRange(endPos, endPos);
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
} else if (option.label === "server-wide") {
|
||||
// For server-wide, add it as a standalone filter with auto-space
|
||||
const newQuery = query + (query ? " " : "") + "server-wide ";
|
||||
|
|
@ -402,8 +737,32 @@ export function SearchBar() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleAttachmentTypeClick = (type: string) => {
|
||||
// Add the attachment type to the query
|
||||
const newQuery = query + type + " ";
|
||||
setQuery(newQuery);
|
||||
setShowAttachmentTypes(false);
|
||||
|
||||
// Move cursor to end and focus
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
const endPos = newQuery.length;
|
||||
inputRef.current.setSelectionRange(endPos, endPos);
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Format date to YYYY-MM-DD in local timezone
|
||||
const formatLocalDate = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
const dateStr = date.toISOString().split('T')[0]; // Format as YYYY-MM-DD for display
|
||||
const dateStr = formatLocalDate(date); // Use local date formatting
|
||||
|
||||
// Add the filter and date to the query with auto-space
|
||||
const filterText = `${showDatePicker}:${dateStr} `;
|
||||
|
|
@ -422,6 +781,35 @@ export function SearchBar() {
|
|||
}, 0);
|
||||
};
|
||||
|
||||
const handleRangeSelect = (startDate: Date, endDate: Date) => {
|
||||
const startStr = formatLocalDate(startDate);
|
||||
const endStr = formatLocalDate(endDate);
|
||||
|
||||
// Store the actual date range in state
|
||||
setActiveDateRange({
|
||||
start: startStr,
|
||||
end: endStr,
|
||||
startDisplay: startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
endDisplay: endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
});
|
||||
|
||||
// Only add "date-range" to the query, not the actual dates
|
||||
const filterText = `date-range `;
|
||||
const newQuery = query + (query ? " " : "") + filterText;
|
||||
|
||||
setQuery(newQuery);
|
||||
setShowDatePicker(null);
|
||||
|
||||
// Move cursor to end after the space
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
const endPos = newQuery.length;
|
||||
inputRef.current.setSelectionRange(endPos, endPos);
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Global keyboard shortcut
|
||||
useEffect(() => {
|
||||
const handleGlobalKeydown = (e: KeyboardEvent) => {
|
||||
|
|
@ -435,21 +823,22 @@ export function SearchBar() {
|
|||
return () => window.removeEventListener("keydown", handleGlobalKeydown);
|
||||
}, []);
|
||||
|
||||
// Close date picker when clicking outside
|
||||
// Close date picker or attachment types when clicking outside
|
||||
useEffect(() => {
|
||||
if (showDatePicker) {
|
||||
if (showDatePicker || showAttachmentTypes) {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
// Check if click is outside the container
|
||||
const container = e.target as HTMLElement;
|
||||
if (!container.closest('[data-search-container]') && !container.closest('[data-date-picker]')) {
|
||||
setShowDatePicker(null);
|
||||
setShowAttachmentTypes(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
}, [showDatePicker]);
|
||||
}, [showDatePicker, showAttachmentTypes]);
|
||||
|
||||
return (
|
||||
<Container data-search-container onMouseDown={(e) => e.stopPropagation()}>
|
||||
|
|
@ -481,7 +870,7 @@ export function SearchBar() {
|
|||
onClick={onAutocompleteClick}
|
||||
/>
|
||||
)}
|
||||
{showOptions && autocompleteState.type === "none" && !showDatePicker && (
|
||||
{showOptions && autocompleteState.type === "none" && !showDatePicker && !showAttachmentTypes && (
|
||||
<OptionsDropdown onClick={(e) => e.stopPropagation()}>
|
||||
<OptionsHeader>{"Search Options"}</OptionsHeader>
|
||||
{searchOptions.map((option) => (
|
||||
|
|
@ -501,9 +890,35 @@ export function SearchBar() {
|
|||
{showDatePicker && (
|
||||
<SearchDatePicker
|
||||
onSelect={handleDateSelect}
|
||||
onRangeSelect={handleRangeSelect}
|
||||
filterType={showDatePicker}
|
||||
/>
|
||||
)}
|
||||
{showAttachmentTypes && (
|
||||
<OptionsDropdown onClick={(e) => e.stopPropagation()}>
|
||||
<OptionsHeader>Attachment Types</OptionsHeader>
|
||||
<Option onClick={() => handleAttachmentTypeClick("video")}>
|
||||
<OptionLabel>video</OptionLabel>
|
||||
<OptionDesc>Messages with videos</OptionDesc>
|
||||
</Option>
|
||||
<Option onClick={() => handleAttachmentTypeClick("image")}>
|
||||
<OptionLabel>image</OptionLabel>
|
||||
<OptionDesc>Messages with images</OptionDesc>
|
||||
</Option>
|
||||
<Option onClick={() => handleAttachmentTypeClick("link")}>
|
||||
<OptionLabel>link</OptionLabel>
|
||||
<OptionDesc>Messages with links</OptionDesc>
|
||||
</Option>
|
||||
<Option onClick={() => handleAttachmentTypeClick("audio")}>
|
||||
<OptionLabel>audio</OptionLabel>
|
||||
<OptionDesc>Messages with audio</OptionDesc>
|
||||
</Option>
|
||||
<Option onClick={() => handleAttachmentTypeClick("file")}>
|
||||
<OptionLabel>file</OptionLabel>
|
||||
<OptionDesc>Messages with files</OptionDesc>
|
||||
</Option>
|
||||
</OptionsDropdown>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -74,16 +74,34 @@ const Days = styled.div`
|
|||
gap: 4px;
|
||||
`;
|
||||
|
||||
const Day = styled.button<{ isToday?: boolean; isSelected?: boolean; isOtherMonth?: boolean }>`
|
||||
background: ${props => props.isSelected ? 'var(--accent)' : 'transparent'};
|
||||
const Day = styled.button<{
|
||||
isToday?: boolean;
|
||||
isSelected?: boolean;
|
||||
isOtherMonth?: boolean;
|
||||
isRangeStart?: boolean;
|
||||
isRangeEnd?: boolean;
|
||||
isInRange?: boolean;
|
||||
isInHoverRange?: boolean;
|
||||
}>`
|
||||
background: ${props => {
|
||||
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent)';
|
||||
if (props.isInRange) return 'var(--accent-disabled)';
|
||||
if (props.isInHoverRange) return 'var(--hover)';
|
||||
return 'transparent';
|
||||
}};
|
||||
color: ${props => {
|
||||
if (props.isSelected) return 'var(--accent-contrast)';
|
||||
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent-contrast)';
|
||||
if (props.isOtherMonth) return 'var(--tertiary-foreground)';
|
||||
return 'var(--foreground)';
|
||||
}};
|
||||
border: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: ${props => {
|
||||
if (props.isRangeStart || (props.isInHoverRange && props.isOtherMonth === false && !props.isRangeEnd)) return '4px 0 0 4px';
|
||||
if (props.isRangeEnd || (props.isInHoverRange && props.isOtherMonth === false && !props.isRangeStart)) return '0 4px 4px 0';
|
||||
if (props.isInRange || props.isInHoverRange) return '0';
|
||||
return '4px';
|
||||
}};
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.1s ease;
|
||||
|
|
@ -99,12 +117,15 @@ const Day = styled.button<{ isToday?: boolean; isSelected?: boolean; isOtherMont
|
|||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
background: ${props.isSelected || props.isRangeStart || props.isRangeEnd ? 'var(--accent-contrast)' : 'var(--accent)'};
|
||||
}
|
||||
`}
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.isSelected ? 'var(--accent)' : 'var(--secondary-background)'};
|
||||
background: ${props => {
|
||||
if (props.isSelected || props.isRangeStart || props.isRangeEnd) return 'var(--accent)';
|
||||
return 'var(--secondary-background)';
|
||||
}};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
|
@ -121,6 +142,22 @@ const QuickSelects = styled.div`
|
|||
border-top: 1px solid var(--secondary-background);
|
||||
`;
|
||||
|
||||
const RangePreview = styled.div`
|
||||
margin: 12px 0;
|
||||
padding: 8px 12px;
|
||||
background: var(--secondary-background);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--foreground);
|
||||
text-align: center;
|
||||
|
||||
.hint {
|
||||
color: var(--tertiary-foreground);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const QuickSelect = styled.button`
|
||||
background: transparent;
|
||||
border: 1px solid var(--secondary-background);
|
||||
|
|
@ -139,12 +176,16 @@ const QuickSelect = styled.button`
|
|||
|
||||
interface Props {
|
||||
onSelect: (date: Date) => void;
|
||||
filterType: "before" | "after" | "during";
|
||||
onRangeSelect?: (startDate: Date, endDate: Date) => void;
|
||||
filterType: "before" | "after" | "during" | "between";
|
||||
}
|
||||
|
||||
export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
||||
export default function SearchDatePicker({ onSelect, onRangeSelect, filterType }: Props) {
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
const [rangeStart, setRangeStart] = useState<Date | null>(null);
|
||||
const [rangeEnd, setRangeEnd] = useState<Date | null>(null);
|
||||
const [hoverDate, setHoverDate] = useState<Date | null>(null);
|
||||
|
||||
const monthNames = ["January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"];
|
||||
|
|
@ -199,9 +240,43 @@ export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
|||
return date1.toDateString() === date2.toDateString();
|
||||
};
|
||||
|
||||
const isDateInRange = (date: Date) => {
|
||||
if (!rangeStart || !rangeEnd) return false;
|
||||
return date >= rangeStart && date <= rangeEnd;
|
||||
};
|
||||
|
||||
const isDateInHoverRange = (date: Date) => {
|
||||
if (filterType !== "between" || !rangeStart || rangeEnd || !hoverDate) return false;
|
||||
const start = rangeStart < hoverDate ? rangeStart : hoverDate;
|
||||
const end = rangeStart < hoverDate ? hoverDate : rangeStart;
|
||||
return date >= start && date <= end;
|
||||
};
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
setSelectedDate(date);
|
||||
onSelect(date);
|
||||
if (filterType === "between") {
|
||||
if (!rangeStart || (rangeStart && rangeEnd)) {
|
||||
// First click or reset
|
||||
setRangeStart(date);
|
||||
setRangeEnd(null);
|
||||
} else {
|
||||
// Second click
|
||||
if (date < rangeStart) {
|
||||
setRangeEnd(rangeStart);
|
||||
setRangeStart(date);
|
||||
} else {
|
||||
setRangeEnd(date);
|
||||
}
|
||||
// Call the callback with both dates
|
||||
if (onRangeSelect) {
|
||||
const start = date < rangeStart ? date : rangeStart;
|
||||
const end = date < rangeStart ? rangeStart : date;
|
||||
onRangeSelect(start, end);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSelectedDate(date);
|
||||
onSelect(date);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickSelect = (option: string) => {
|
||||
|
|
@ -246,7 +321,10 @@ export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
|||
<ChevronLeft size={20} />
|
||||
</NavButton>
|
||||
<MonthYear>
|
||||
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
||||
{filterType === "between" && rangeStart && !rangeEnd
|
||||
? "Select end date"
|
||||
: `${monthNames[currentMonth.getMonth()]} ${currentMonth.getFullYear()}`
|
||||
}
|
||||
</MonthYear>
|
||||
<NavButton onClick={() => navigateMonth(1)}>
|
||||
<ChevronRight size={20} />
|
||||
|
|
@ -264,8 +342,14 @@ export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
|||
<Day
|
||||
key={index}
|
||||
onClick={() => handleDateSelect(day.date)}
|
||||
onMouseEnter={() => setHoverDate(day.date)}
|
||||
onMouseLeave={() => setHoverDate(null)}
|
||||
isToday={isToday(day.date)}
|
||||
isSelected={isSameDate(day.date, selectedDate)}
|
||||
isSelected={filterType !== "between" && isSameDate(day.date, selectedDate)}
|
||||
isRangeStart={filterType === "between" && rangeStart && isSameDate(day.date, rangeStart)}
|
||||
isRangeEnd={filterType === "between" && rangeEnd && isSameDate(day.date, rangeEnd)}
|
||||
isInRange={filterType === "between" && isDateInRange(day.date)}
|
||||
isInHoverRange={isDateInHoverRange(day.date)}
|
||||
isOtherMonth={day.isOtherMonth}
|
||||
>
|
||||
{day.date.getDate()}
|
||||
|
|
@ -273,6 +357,7 @@ export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
|||
))}
|
||||
</Days>
|
||||
|
||||
|
||||
<QuickSelects>
|
||||
<QuickSelect onClick={() => handleQuickSelect("today")}>
|
||||
Today
|
||||
|
|
|
|||
|
|
@ -109,9 +109,9 @@ interface Props {
|
|||
query: string;
|
||||
author?: string;
|
||||
mention?: string;
|
||||
before_date?: string;
|
||||
after_date?: string;
|
||||
during?: string;
|
||||
date_start?: string;
|
||||
date_end?: string;
|
||||
has?: string;
|
||||
server_wide?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
@ -132,7 +132,8 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
async function search() {
|
||||
const searchQuery = searchParams?.query || query;
|
||||
if (!searchQuery && !searchParams?.author && !searchParams?.mention &&
|
||||
!searchParams?.before_date && !searchParams?.after_date && !searchParams?.during && !searchParams?.server_wide) return;
|
||||
!searchParams?.date_start && !searchParams?.date_end &&
|
||||
!searchParams?.has && !searchParams?.server_wide) return;
|
||||
|
||||
setState({ type: "loading" });
|
||||
const searchOptions: any = {
|
||||
|
|
@ -148,15 +149,12 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
searchOptions.mention = searchParams.mention;
|
||||
}
|
||||
|
||||
// Add date filters if provided
|
||||
if (searchParams?.before_date) {
|
||||
searchOptions.before_date = searchParams.before_date;
|
||||
// Add date filters if provided using the new standardized parameters
|
||||
if (searchParams?.date_start) {
|
||||
searchOptions.date_start = searchParams.date_start;
|
||||
}
|
||||
if (searchParams?.after_date) {
|
||||
searchOptions.after_date = searchParams.after_date;
|
||||
}
|
||||
if (searchParams?.during) {
|
||||
searchOptions.during = searchParams.during;
|
||||
if (searchParams?.date_end) {
|
||||
searchOptions.date_end = searchParams.date_end;
|
||||
}
|
||||
|
||||
// Add server-wide filter if provided
|
||||
|
|
@ -164,6 +162,11 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
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 });
|
||||
}
|
||||
|
|
@ -199,7 +202,14 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
</div>
|
||||
{state.type === "loading" && <Preloader type="ring" />}
|
||||
{state.type === "results" && (
|
||||
<div className="list">
|
||||
<>
|
||||
<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) => {
|
||||
|
|
@ -256,7 +266,8 @@ export function SearchSidebar({ close, initialQuery = "", searchParams }: Props)
|
|||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SearchBase>
|
||||
</GenericSidebarList>
|
||||
|
|
|
|||
|
|
@ -36,19 +36,19 @@ export function useSearchAutoComplete(
|
|||
const beforeCursor = value.slice(0, cursorPos);
|
||||
const afterCursor = value.slice(cursorPos);
|
||||
|
||||
// Check if we're currently typing after "from:" or "mentions:"
|
||||
const fromMatch = beforeCursor.match(/\bfrom:(@?\w*)$/);
|
||||
// Check if we're currently typing after "from:" or "mentions:" (including hyphens)
|
||||
const fromMatch = beforeCursor.match(/\bfrom:(@?[\w-]*)$/);
|
||||
if (fromMatch) {
|
||||
// Check if there's already a username that continues after cursor
|
||||
const continuationMatch = afterCursor.match(/^(\w*)/);
|
||||
const continuationMatch = afterCursor.match(/^([\w-]*)/);
|
||||
const fullUsername = fromMatch[1].replace('@', '') + (continuationMatch ? continuationMatch[1] : '');
|
||||
return ["user", fullUsername, fromMatch.index! + 5, "from"];
|
||||
}
|
||||
|
||||
const mentionsMatch = beforeCursor.match(/\bmentions:(@?\w*)$/);
|
||||
const mentionsMatch = beforeCursor.match(/\bmentions:(@?[\w-]*)$/);
|
||||
if (mentionsMatch) {
|
||||
// Check if there's already a username that continues after cursor
|
||||
const continuationMatch = afterCursor.match(/^(\w*)/);
|
||||
const continuationMatch = afterCursor.match(/^([\w-]*)/);
|
||||
const fullUsername = mentionsMatch[1].replace('@', '') + (continuationMatch ? continuationMatch[1] : '');
|
||||
return ["user", fullUsername, mentionsMatch.index! + 9, "mentions"];
|
||||
}
|
||||
|
|
@ -65,9 +65,9 @@ export function useSearchAutoComplete(
|
|||
// Check if we're still within this filter (no space between filter and cursor)
|
||||
const betweenFilterAndCursor = value.slice(filterIndex + filterLength, cursorPos);
|
||||
if (!betweenFilterAndCursor.includes(" ")) {
|
||||
// Get the username part
|
||||
// Get the username part (including hyphens)
|
||||
const afterFilter = value.slice(filterIndex + filterLength);
|
||||
const usernameMatch = afterFilter.match(/^@?(\w*)/);
|
||||
const usernameMatch = afterFilter.match(/^@?([\w-]*)/);
|
||||
if (usernameMatch) {
|
||||
return ["user", usernameMatch[1] || "", filterIndex + filterLength, filterType];
|
||||
}
|
||||
|
|
@ -142,10 +142,10 @@ export function useSearchAutoComplete(
|
|||
newMappings[mappingKey] = selectedUser._id;
|
||||
setUserMappings(newMappings);
|
||||
|
||||
// Find the end of the current username (including @ if present)
|
||||
// Find the end of the current username (including @ if present and hyphens)
|
||||
let endIndex = startIndex;
|
||||
const afterStartIndex = el.value.slice(startIndex);
|
||||
const existingMatch = afterStartIndex.match(/^@?\w*/);
|
||||
const existingMatch = afterStartIndex.match(/^@?[\w-]*/);
|
||||
if (existingMatch) {
|
||||
endIndex = startIndex + existingMatch[0].length;
|
||||
}
|
||||
|
|
@ -254,17 +254,17 @@ export function useSearchAutoComplete(
|
|||
export function transformSearchQuery(
|
||||
displayQuery: string,
|
||||
userMappings: UserMapping
|
||||
): { query: string; author?: string; mention?: string; before_date?: string; after_date?: string; during?: string; server_wide?: boolean } {
|
||||
): { query: string; author?: string; mention?: string; date_start?: string; date_end?: string; has?: string; server_wide?: boolean } {
|
||||
let query = displayQuery;
|
||||
let author: string | undefined;
|
||||
let mention: string | undefined;
|
||||
let before_date: string | undefined;
|
||||
let after_date: string | undefined;
|
||||
let during: string | undefined;
|
||||
let date_start: string | undefined;
|
||||
let date_end: string | undefined;
|
||||
let has: string | undefined;
|
||||
let server_wide: boolean | undefined;
|
||||
|
||||
// Extract and replace from:@username with user ID
|
||||
const fromMatch = query.match(/\bfrom:@?(\w+)/);
|
||||
// Extract and replace from:@username with user ID (including hyphens)
|
||||
const fromMatch = query.match(/\bfrom:@?([\w-]+)/);
|
||||
if (fromMatch) {
|
||||
const username = fromMatch[1];
|
||||
const userId = userMappings[`from:${username}`];
|
||||
|
|
@ -275,8 +275,8 @@ export function transformSearchQuery(
|
|||
}
|
||||
}
|
||||
|
||||
// Extract and replace mentions:@username with user ID
|
||||
const mentionsMatch = query.match(/\bmentions:@?(\w+)/);
|
||||
// Extract and replace mentions:@username with user ID (including hyphens)
|
||||
const mentionsMatch = query.match(/\bmentions:@?([\w-]+)/);
|
||||
if (mentionsMatch) {
|
||||
const username = mentionsMatch[1];
|
||||
const userId = userMappings[`mentions:${username}`];
|
||||
|
|
@ -288,35 +288,76 @@ export function transformSearchQuery(
|
|||
}
|
||||
|
||||
// Extract date filters (YYYY-MM-DD format) and convert to ISO 8601
|
||||
// Using the new standardized date_start and date_end approach
|
||||
const beforeMatch = query.match(/\bbefore:(\d{4}-\d{2}-\d{2})/);
|
||||
if (beforeMatch) {
|
||||
// Convert YYYY-MM-DD to full ISO 8601 format
|
||||
const date = new Date(beforeMatch[1]);
|
||||
before_date = date.toISOString();
|
||||
// "before" means before the START of this day
|
||||
const [year, month, day] = beforeMatch[1].split('-').map(Number);
|
||||
const startOfDay = new Date(year, month - 1, day);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
|
||||
date_end = startOfDay.toISOString();
|
||||
query = query.replace(beforeMatch[0], "").trim();
|
||||
}
|
||||
|
||||
const afterMatch = query.match(/\bafter:(\d{4}-\d{2}-\d{2})/);
|
||||
if (afterMatch) {
|
||||
// Convert YYYY-MM-DD to full ISO 8601 format
|
||||
const date = new Date(afterMatch[1]);
|
||||
after_date = date.toISOString();
|
||||
// "after" means after the END of this day
|
||||
const [year, month, day] = afterMatch[1].split('-').map(Number);
|
||||
const endOfDay = new Date(year, month - 1, day);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
|
||||
date_start = endOfDay.toISOString();
|
||||
query = query.replace(afterMatch[0], "").trim();
|
||||
}
|
||||
|
||||
const duringMatch = query.match(/\bduring:(\d{4}-\d{2}-\d{2})/);
|
||||
if (duringMatch) {
|
||||
// Convert YYYY-MM-DD to full ISO 8601 format
|
||||
const date = new Date(duringMatch[1]);
|
||||
during = date.toISOString();
|
||||
// For 'during', capture the full day from start to end
|
||||
const [year, month, day] = duringMatch[1].split('-').map(Number);
|
||||
|
||||
const startOfDay = new Date(year, month - 1, day);
|
||||
startOfDay.setHours(0, 0, 0, 0);
|
||||
|
||||
const endOfDay = new Date(year, month - 1, day);
|
||||
endOfDay.setHours(23, 59, 59, 999);
|
||||
|
||||
date_start = startOfDay.toISOString();
|
||||
date_end = endOfDay.toISOString();
|
||||
|
||||
query = query.replace(duringMatch[0], "").trim();
|
||||
}
|
||||
|
||||
// Extract between date range filter (between:YYYY-MM-DD..YYYY-MM-DD)
|
||||
const betweenMatch = query.match(/\bbetween:(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})/);
|
||||
if (betweenMatch) {
|
||||
// Start date: from the START of the first day
|
||||
const [startYear, startMonth, startDay] = betweenMatch[1].split('-').map(Number);
|
||||
const startOfFirstDay = new Date(startYear, startMonth - 1, startDay);
|
||||
startOfFirstDay.setHours(0, 0, 0, 0);
|
||||
date_start = startOfFirstDay.toISOString();
|
||||
|
||||
// End date: to the END of the last day
|
||||
const [endYear, endMonth, endDay] = betweenMatch[2].split('-').map(Number);
|
||||
const endOfLastDay = new Date(endYear, endMonth - 1, endDay);
|
||||
endOfLastDay.setHours(23, 59, 59, 999);
|
||||
date_end = endOfLastDay.toISOString();
|
||||
|
||||
query = query.replace(betweenMatch[0], "").trim();
|
||||
}
|
||||
|
||||
// Extract has: filter for attachment types
|
||||
const hasMatch = query.match(/\bhas:(video|image|link|audio|file)/i);
|
||||
if (hasMatch) {
|
||||
has = hasMatch[1].toLowerCase();
|
||||
query = query.replace(hasMatch[0], "").trim();
|
||||
}
|
||||
|
||||
// Check for server-wide flag
|
||||
if (query.includes("server-wide")) {
|
||||
server_wide = true;
|
||||
query = query.replace(/\bserver-wide\b/g, "").trim();
|
||||
}
|
||||
|
||||
return { query, author, mention, before_date, after_date, during, server_wide };
|
||||
return { query, author, mention, date_start, date_end, has, server_wide };
|
||||
}
|
||||
Loading…
Reference in New Issue