mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-08 09:55:28 +00:00
New Search UI
This commit is contained in:
2
external/revolt.js
vendored
2
external/revolt.js
vendored
Submodule external/revolt.js updated: 62d4a668b2...9ab546e443
@@ -10,21 +10,48 @@ import { SearchSidebar } from "./right/Search";
|
|||||||
|
|
||||||
export default function RightSidebar() {
|
export default function RightSidebar() {
|
||||||
const [sidebar, setSidebar] = useState<"search" | undefined>();
|
const [sidebar, setSidebar] = useState<"search" | undefined>();
|
||||||
const close = () => setSidebar(undefined);
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [searchParams, setSearchParams] = useState<any>(null);
|
||||||
|
const close = () => {
|
||||||
|
setSidebar(undefined);
|
||||||
|
setSearchQuery("");
|
||||||
|
setSearchParams(null);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
internalSubscribe(
|
internalSubscribe("RightSidebar", "open", (type: string, data?: any) => {
|
||||||
"RightSidebar",
|
setSidebar(type as "search" | undefined);
|
||||||
"open",
|
if (type === "search") {
|
||||||
setSidebar as (...args: unknown[]) => void,
|
if (typeof data === "string") {
|
||||||
),
|
// Legacy support for string queries
|
||||||
[setSidebar],
|
setSearchQuery(data);
|
||||||
|
setSearchParams(null);
|
||||||
|
} else if (data?.query !== undefined) {
|
||||||
|
// New format with search parameters
|
||||||
|
setSearchQuery(data.query);
|
||||||
|
setSearchParams(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
internalSubscribe("RightSidebar", "close", () => {
|
||||||
|
close();
|
||||||
|
}),
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const content =
|
const content =
|
||||||
sidebar === "search" ? (
|
sidebar === "search" ? (
|
||||||
<SearchSidebar close={close} />
|
<SearchSidebar
|
||||||
|
close={close}
|
||||||
|
initialQuery={searchQuery}
|
||||||
|
searchParams={searchParams}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MemberSidebar />
|
<MemberSidebar />
|
||||||
);
|
);
|
||||||
|
|||||||
101
src/components/navigation/SearchAutoComplete.tsx
Normal file
101
src/components/navigation/SearchAutoComplete.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import styled from "styled-components/macro";
|
||||||
|
import { User } from "revolt.js";
|
||||||
|
import { AutoCompleteState } from "../common/AutoComplete";
|
||||||
|
import UserIcon from "../common/user/UserIcon";
|
||||||
|
|
||||||
|
const Base = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--primary-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1001;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
font-family: inherit;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
state: AutoCompleteState;
|
||||||
|
setState: (state: AutoCompleteState) => void;
|
||||||
|
onClick: (userId: string, username: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchAutoComplete({ state, setState, onClick }: Props) {
|
||||||
|
if (state.type !== "user") return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base>
|
||||||
|
{state.matches.length > 0 ? (
|
||||||
|
state.matches.map((user, i) => (
|
||||||
|
<button
|
||||||
|
key={user._id}
|
||||||
|
className={i === state.selected ? "active" : ""}
|
||||||
|
onMouseEnter={() =>
|
||||||
|
(i !== state.selected || !state.within) &&
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
selected: i,
|
||||||
|
within: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMouseLeave={() =>
|
||||||
|
state.within &&
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
within: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick(user._id, user.username);
|
||||||
|
}}>
|
||||||
|
<UserIcon size={20} target={user} status={true} />
|
||||||
|
<span>{user.username}</span>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
padding: "12px",
|
||||||
|
textAlign: "center",
|
||||||
|
color: "var(--tertiary-foreground)",
|
||||||
|
fontSize: "13px"
|
||||||
|
}}>
|
||||||
|
No users found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
}
|
||||||
509
src/components/navigation/SearchBar.tsx
Normal file
509
src/components/navigation/SearchBar.tsx
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
import { Search, X } from "@styled-icons/boxicons-regular";
|
||||||
|
import { HelpCircle } from "@styled-icons/boxicons-solid";
|
||||||
|
import styled from "styled-components/macro";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { Tooltip } from "@revoltchat/ui";
|
||||||
|
import { internalEmit } from "../../lib/eventEmitter";
|
||||||
|
import { useSearchAutoComplete, transformSearchQuery, UserMapping } from "../../lib/hooks/useSearchAutoComplete";
|
||||||
|
import SearchAutoComplete from "./SearchAutoComplete";
|
||||||
|
import SearchDatePicker from "./SearchDatePicker";
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--primary-header);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
width: 220px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Input = styled.input`
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 2px 6px 12px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconButton = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px 0 8px;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.1s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionsDropdown = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: var(--primary-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 300px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionsHeader = styled.div`
|
||||||
|
padding: 0 8px 8px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Option = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionLabel = styled.span`
|
||||||
|
color: var(--foreground);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-family: var(--monospace-font), monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const OptionDesc = styled.span`
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
font-size: 13px;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HelpIcon = styled(HelpCircle)`
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
|
${Option}:hover & {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface SearchOption {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchOptions: SearchOption[] = [
|
||||||
|
{
|
||||||
|
label: "from:",
|
||||||
|
description: "user",
|
||||||
|
tooltip: "Filter messages by author"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "mentions:",
|
||||||
|
description: "user",
|
||||||
|
tooltip: "Find messages mentioning a user"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "before:",
|
||||||
|
description: "specific date",
|
||||||
|
tooltip: "Messages before this date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "during:",
|
||||||
|
description: "specific date",
|
||||||
|
tooltip: "Messages on this date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "after:",
|
||||||
|
description: "specific date",
|
||||||
|
tooltip: "Messages after this date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "server-wide",
|
||||||
|
description: "Entire server",
|
||||||
|
tooltip: "Search in entire server instead of just this channel"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SearchBar() {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
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 inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Setup autocomplete
|
||||||
|
const {
|
||||||
|
state: autocompleteState,
|
||||||
|
setState: setAutocompleteState,
|
||||||
|
onKeyUp,
|
||||||
|
onKeyDown: onAutocompleteKeyDown,
|
||||||
|
onChange: onAutocompleteChange,
|
||||||
|
onClick: onAutocompleteClick,
|
||||||
|
onFocus: onAutocompleteFocus,
|
||||||
|
onBlur: onAutocompleteBlur,
|
||||||
|
} = useSearchAutoComplete(setQuery, userMappings, setUserMappings);
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
onAutocompleteFocus();
|
||||||
|
setShowOptions(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
// Show options when clicking on the input, even if already focused
|
||||||
|
if (!showOptions && autocompleteState.type === "none" && !showDatePicker) {
|
||||||
|
setShowOptions(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
onAutocompleteBlur();
|
||||||
|
// Delay to allow clicking on options
|
||||||
|
setTimeout(() => {
|
||||||
|
// Check if we have an incomplete filter
|
||||||
|
const hasIncompleteFilter = query.match(/\b(from:|mentions:)\s*$/);
|
||||||
|
const hasIncompleteDateFilter = query.match(/\b(before:|after:|during:)\s*$/);
|
||||||
|
|
||||||
|
if (!hasIncompleteFilter && !hasIncompleteDateFilter && !showDatePicker) {
|
||||||
|
setShowOptions(false);
|
||||||
|
if (autocompleteState.type === "none") {
|
||||||
|
setAutocompleteState({ type: "none" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (e: Event) => {
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
const cursorPos = (e.target as HTMLInputElement).selectionStart || 0;
|
||||||
|
|
||||||
|
// Check if user is trying to add space after incomplete filter
|
||||||
|
const incompleteFilterWithSpace = value.match(/\b(from:|mentions:)\s+$/);
|
||||||
|
if (incompleteFilterWithSpace && autocompleteState.type === "user") {
|
||||||
|
// Don't allow space after filter unless user was selected
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery(value);
|
||||||
|
|
||||||
|
// Check for date 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*$/);
|
||||||
|
|
||||||
|
if (beforeMatch) {
|
||||||
|
setShowDatePicker("before");
|
||||||
|
setShowOptions(false);
|
||||||
|
setAutocompleteState({ type: "none" });
|
||||||
|
} else if (afterMatch) {
|
||||||
|
setShowDatePicker("after");
|
||||||
|
setShowOptions(false);
|
||||||
|
setAutocompleteState({ type: "none" });
|
||||||
|
} else if (duringMatch) {
|
||||||
|
setShowDatePicker("during");
|
||||||
|
setShowOptions(false);
|
||||||
|
setAutocompleteState({ type: "none" });
|
||||||
|
} else {
|
||||||
|
// Only trigger autocomplete if no date filter is active
|
||||||
|
const dateFilterActive = value.match(/\b(before:|after:|during:)\s*$/);
|
||||||
|
if (!dateFilterActive) {
|
||||||
|
onAutocompleteChange(e);
|
||||||
|
if (showDatePicker) {
|
||||||
|
setShowDatePicker(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
|
||||||
|
// Check for incomplete filters (only user filters, not date filters)
|
||||||
|
const hasIncompleteUserFilter = trimmedQuery.match(/\b(from:|mentions:)\s*$/);
|
||||||
|
const hasIncompleteDateFilter = trimmedQuery.match(/\b(before:|after:|during:)\s*$/);
|
||||||
|
|
||||||
|
if (hasIncompleteUserFilter || hasIncompleteDateFilter) {
|
||||||
|
// Don't search if there's an incomplete filter
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (hasSearchCriteria) {
|
||||||
|
// Open search in right sidebar with transformed query
|
||||||
|
internalEmit("RightSidebar", "open", "search", searchParams);
|
||||||
|
setShowOptions(false);
|
||||||
|
setIsSearching(true);
|
||||||
|
setAutocompleteState({ type: "none" });
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const currentValue = (e.currentTarget as HTMLInputElement).value;
|
||||||
|
const cursorPos = (e.currentTarget as HTMLInputElement).selectionStart || 0;
|
||||||
|
|
||||||
|
// Handle backspace/delete for 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
|
||||||
|
if (e.key === "Backspace") {
|
||||||
|
const serverWideMatch = beforeCursor.match(/\bserver-wide\s*$/);
|
||||||
|
if (serverWideMatch) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newValue = currentValue.slice(0, serverWideMatch.index) + afterCursor;
|
||||||
|
setQuery(newValue);
|
||||||
|
// Set cursor position to where server-wide started
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.setSelectionRange(serverWideMatch.index, serverWideMatch.index);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (e.key === "Delete") {
|
||||||
|
// Check if cursor is at the beginning of server-wide
|
||||||
|
const serverWideAfter = afterCursor.match(/^server-wide\s*/);
|
||||||
|
if (serverWideAfter) {
|
||||||
|
e.preventDefault();
|
||||||
|
const newValue = beforeCursor + afterCursor.slice(serverWideAfter[0].length);
|
||||||
|
setQuery(newValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is trying to type space when there's an incomplete filter
|
||||||
|
if (e.key === " " || e.key === "Spacebar") {
|
||||||
|
const beforeCursor = currentValue.slice(0, cursorPos);
|
||||||
|
const afterCursor = currentValue.slice(cursorPos);
|
||||||
|
|
||||||
|
// Check if cursor is within or right after a filter
|
||||||
|
const lastFromIndex = beforeCursor.lastIndexOf("from:");
|
||||||
|
const lastMentionsIndex = beforeCursor.lastIndexOf("mentions:");
|
||||||
|
|
||||||
|
if (lastFromIndex !== -1 || lastMentionsIndex !== -1) {
|
||||||
|
const filterIndex = Math.max(lastFromIndex, lastMentionsIndex);
|
||||||
|
const afterFilter = beforeCursor.slice(filterIndex);
|
||||||
|
|
||||||
|
// Check if we're still within the filter (no space after it in the part before cursor)
|
||||||
|
if (!afterFilter.includes(" ")) {
|
||||||
|
// Also check if there's no space immediately after cursor (editing in middle of username)
|
||||||
|
const hasSpaceAfterCursor = afterCursor.startsWith(" ");
|
||||||
|
|
||||||
|
if (!hasSpaceAfterCursor) {
|
||||||
|
// We're within a filter and trying to add space - always prevent
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let autocomplete handle key events first
|
||||||
|
if (onAutocompleteKeyDown(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
// Don't search if autocomplete is showing
|
||||||
|
if (autocompleteState.type === "none") {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
if (query) {
|
||||||
|
setQuery("");
|
||||||
|
} else {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
if (isSearching) {
|
||||||
|
internalEmit("RightSidebar", "close");
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setQuery("");
|
||||||
|
setIsSearching(false);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
internalEmit("RightSidebar", "close");
|
||||||
|
};
|
||||||
|
|
||||||
|
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");
|
||||||
|
setShowOptions(false);
|
||||||
|
} else if (option.label === "server-wide") {
|
||||||
|
// For server-wide, add it as a standalone filter with auto-space
|
||||||
|
const newQuery = query + (query ? " " : "") + "server-wide ";
|
||||||
|
setQuery(newQuery);
|
||||||
|
setShowOptions(false);
|
||||||
|
|
||||||
|
// Move cursor to end after the space
|
||||||
|
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);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (date: Date) => {
|
||||||
|
const dateStr = date.toISOString().split('T')[0]; // Format as YYYY-MM-DD for display
|
||||||
|
|
||||||
|
// Add the filter and date to the query with auto-space
|
||||||
|
const filterText = `${showDatePicker}:${dateStr} `;
|
||||||
|
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) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
|
e.preventDefault();
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleGlobalKeydown);
|
||||||
|
return () => window.removeEventListener("keydown", handleGlobalKeydown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close date picker when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
if (showDatePicker) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [showDatePicker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container data-search-container onMouseDown={(e) => e.stopPropagation()}>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
value={query}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onClick={handleClick}
|
||||||
|
onInput={handleInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
/>
|
||||||
|
{isSearching ? (
|
||||||
|
<IconButton onClick={handleClear}>
|
||||||
|
<X size={18} />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={handleSearch}>
|
||||||
|
<Search size={18} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{autocompleteState.type !== "none" && (
|
||||||
|
<SearchAutoComplete
|
||||||
|
state={autocompleteState}
|
||||||
|
setState={setAutocompleteState}
|
||||||
|
onClick={onAutocompleteClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showOptions && autocompleteState.type === "none" && !showDatePicker && (
|
||||||
|
<OptionsDropdown onClick={(e) => e.stopPropagation()}>
|
||||||
|
<OptionsHeader>{"Search Options"}</OptionsHeader>
|
||||||
|
{searchOptions.map((option) => (
|
||||||
|
<Option
|
||||||
|
key={option.label}
|
||||||
|
onClick={() => handleOptionClick(option)}
|
||||||
|
>
|
||||||
|
<OptionLabel>{option.label}</OptionLabel>
|
||||||
|
<OptionDesc>{option.description}</OptionDesc>
|
||||||
|
<Tooltip content={option.tooltip} placement="right">
|
||||||
|
<HelpIcon size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</OptionsDropdown>
|
||||||
|
)}
|
||||||
|
{showDatePicker && (
|
||||||
|
<SearchDatePicker
|
||||||
|
onSelect={handleDateSelect}
|
||||||
|
filterType={showDatePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
292
src/components/navigation/SearchDatePicker.tsx
Normal file
292
src/components/navigation/SearchDatePicker.tsx
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import styled from "styled-components/macro";
|
||||||
|
import { useState, useEffect, useRef } from "preact/hooks";
|
||||||
|
import { ChevronLeft, ChevronRight } from "@styled-icons/boxicons-regular";
|
||||||
|
|
||||||
|
const Base = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: var(--primary-background);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1002;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 12px;
|
||||||
|
min-width: 280px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
min-width: 240px;
|
||||||
|
right: -20px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MonthYear = styled.div`
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NavButton = styled.button`
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WeekDays = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const WeekDay = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--tertiary-foreground);
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Days = styled.div`
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Day = styled.button<{ isToday?: boolean; isSelected?: boolean; isOtherMonth?: boolean }>`
|
||||||
|
background: ${props => props.isSelected ? 'var(--accent)' : 'transparent'};
|
||||||
|
color: ${props => {
|
||||||
|
if (props.isSelected) return 'var(--accent-contrast)';
|
||||||
|
if (props.isOtherMonth) return 'var(--tertiary-foreground)';
|
||||||
|
return 'var(--foreground)';
|
||||||
|
}};
|
||||||
|
border: none;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${props => props.isToday && `
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${props => props.isSelected ? 'var(--accent)' : 'var(--secondary-background)'};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QuickSelects = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--secondary-background);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QuickSelect = styled.button`
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--secondary-background);
|
||||||
|
color: var(--foreground);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
border-color: var(--tertiary-background);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onSelect: (date: Date) => void;
|
||||||
|
filterType: "before" | "after" | "during";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchDatePicker({ onSelect, filterType }: Props) {
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const monthNames = ["January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December"];
|
||||||
|
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
|
||||||
|
const getDaysInMonth = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysInMonth = lastDay.getDate();
|
||||||
|
const startingDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
const days: Array<{ date: Date; isOtherMonth: boolean }> = [];
|
||||||
|
|
||||||
|
// Previous month days
|
||||||
|
const prevMonthLastDay = new Date(year, month, 0).getDate();
|
||||||
|
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
||||||
|
days.push({
|
||||||
|
date: new Date(year, month - 1, prevMonthLastDay - i),
|
||||||
|
isOtherMonth: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current month days
|
||||||
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
|
days.push({
|
||||||
|
date: new Date(year, month, i),
|
||||||
|
isOtherMonth: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next month days to fill the grid
|
||||||
|
const remainingDays = 42 - days.length; // 6 weeks * 7 days
|
||||||
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
|
days.push({
|
||||||
|
date: new Date(year, month + 1, i),
|
||||||
|
isOtherMonth: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = (date: Date) => {
|
||||||
|
const today = new Date();
|
||||||
|
return date.toDateString() === today.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSameDate = (date1: Date, date2: Date | null) => {
|
||||||
|
if (!date2) return false;
|
||||||
|
return date1.toDateString() === date2.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateSelect = (date: Date) => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
onSelect(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickSelect = (option: string) => {
|
||||||
|
const today = new Date();
|
||||||
|
let date: Date;
|
||||||
|
|
||||||
|
switch (option) {
|
||||||
|
case "today":
|
||||||
|
date = today;
|
||||||
|
break;
|
||||||
|
case "yesterday":
|
||||||
|
date = new Date(today);
|
||||||
|
date.setDate(today.getDate() - 1);
|
||||||
|
break;
|
||||||
|
case "week":
|
||||||
|
date = new Date(today);
|
||||||
|
date.setDate(today.getDate() - 7);
|
||||||
|
break;
|
||||||
|
case "month":
|
||||||
|
date = new Date(today);
|
||||||
|
date.setMonth(today.getMonth() - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDateSelect(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateMonth = (direction: number) => {
|
||||||
|
const newMonth = new Date(currentMonth);
|
||||||
|
newMonth.setMonth(currentMonth.getMonth() + direction);
|
||||||
|
setCurrentMonth(newMonth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const days = getDaysInMonth(currentMonth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Base data-date-picker onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Header>
|
||||||
|
<NavButton onClick={() => navigateMonth(-1)}>
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</NavButton>
|
||||||
|
<MonthYear>
|
||||||
|
{monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
||||||
|
</MonthYear>
|
||||||
|
<NavButton onClick={() => navigateMonth(1)}>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</NavButton>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<WeekDays>
|
||||||
|
{weekDays.map(day => (
|
||||||
|
<WeekDay key={day}>{day}</WeekDay>
|
||||||
|
))}
|
||||||
|
</WeekDays>
|
||||||
|
|
||||||
|
<Days>
|
||||||
|
{days.map((day, index) => (
|
||||||
|
<Day
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleDateSelect(day.date)}
|
||||||
|
isToday={isToday(day.date)}
|
||||||
|
isSelected={isSameDate(day.date, selectedDate)}
|
||||||
|
isOtherMonth={day.isOtherMonth}
|
||||||
|
>
|
||||||
|
{day.date.getDate()}
|
||||||
|
</Day>
|
||||||
|
))}
|
||||||
|
</Days>
|
||||||
|
|
||||||
|
<QuickSelects>
|
||||||
|
<QuickSelect onClick={() => handleQuickSelect("today")}>
|
||||||
|
Today
|
||||||
|
</QuickSelect>
|
||||||
|
<QuickSelect onClick={() => handleQuickSelect("yesterday")}>
|
||||||
|
Yesterday
|
||||||
|
</QuickSelect>
|
||||||
|
<QuickSelect onClick={() => handleQuickSelect("week")}>
|
||||||
|
Last week
|
||||||
|
</QuickSelect>
|
||||||
|
<QuickSelect onClick={() => handleQuickSelect("month")}>
|
||||||
|
Last month
|
||||||
|
</QuickSelect>
|
||||||
|
</QuickSelects>
|
||||||
|
</Base>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams, useHistory } from "react-router-dom";
|
||||||
import { Message as MessageI } from "revolt.js";
|
import { Message as MessageI } from "revolt.js";
|
||||||
import styled from "styled-components/macro";
|
import styled from "styled-components/macro";
|
||||||
|
|
||||||
import { Text } from "preact-i18n";
|
import { Text } from "preact-i18n";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { Button, Category, Error, InputBox, Preloader } from "@revoltchat/ui";
|
import { Button, Preloader } from "@revoltchat/ui";
|
||||||
|
|
||||||
import { useClient } from "../../../controllers/client/ClientController";
|
import { useClient } from "../../../controllers/client/ClientController";
|
||||||
|
import { internalEmit } from "../../../lib/eventEmitter";
|
||||||
import Message from "../../common/messaging/Message";
|
import Message from "../../common/messaging/Message";
|
||||||
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
import { GenericSidebarBase, GenericSidebarList } from "../SidebarBase";
|
||||||
|
|
||||||
@@ -23,8 +24,22 @@ type SearchState =
|
|||||||
results: MessageI[];
|
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`
|
const SearchBase = styled.div`
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
padding-top: 48px; /* Add space for the header */
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -38,12 +53,10 @@ const SearchBase = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin: 2px;
|
margin: 4px 2px 8px 2px;
|
||||||
padding: 6px;
|
padding: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
color: var(--foreground);
|
|
||||||
background: var(--primary-background);
|
background: var(--primary-background);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -53,6 +66,14 @@ const SearchBase = styled.div`
|
|||||||
> * {
|
> * {
|
||||||
pointer-events: none;
|
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 {
|
.sort {
|
||||||
@@ -67,57 +88,109 @@ const SearchBase = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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 {
|
interface Props {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
initialQuery?: string;
|
||||||
|
searchParams?: {
|
||||||
|
query: string;
|
||||||
|
author?: string;
|
||||||
|
mention?: string;
|
||||||
|
before_date?: string;
|
||||||
|
after_date?: string;
|
||||||
|
during?: string;
|
||||||
|
server_wide?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchSidebar({ close }: Props) {
|
export function SearchSidebar({ close, initialQuery = "", searchParams }: Props) {
|
||||||
const channel = useClient().channels.get(
|
const client = useClient();
|
||||||
useParams<{ channel: string }>().channel,
|
const history = useHistory();
|
||||||
)!;
|
const params = useParams<{ channel: string }>();
|
||||||
|
const channel = client.channels.get(params.channel)!;
|
||||||
|
|
||||||
type Sort = "Relevance" | "Latest" | "Oldest";
|
type Sort = "Relevance" | "Latest" | "Oldest";
|
||||||
const [sort, setSort] = useState<Sort>("Latest");
|
const [sort, setSort] = useState<Sort>("Latest");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState(searchParams?.query || initialQuery);
|
||||||
|
|
||||||
const [state, setState] = useState<SearchState>({ type: "waiting" });
|
const [state, setState] = useState<SearchState>({ type: "waiting" });
|
||||||
|
const [savedSearchParams, setSavedSearchParams] = useState(searchParams);
|
||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
if (!query) return;
|
const searchQuery = searchParams?.query || query;
|
||||||
|
if (!searchQuery && !searchParams?.author && !searchParams?.mention &&
|
||||||
|
!searchParams?.before_date && !searchParams?.after_date && !searchParams?.during && !searchParams?.server_wide) return;
|
||||||
|
|
||||||
setState({ type: "loading" });
|
setState({ type: "loading" });
|
||||||
const data = await channel.searchWithUsers({ query, sort });
|
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
|
||||||
|
if (searchParams?.before_date) {
|
||||||
|
searchOptions.before_date = searchParams.before_date;
|
||||||
|
}
|
||||||
|
if (searchParams?.after_date) {
|
||||||
|
searchOptions.after_date = searchParams.after_date;
|
||||||
|
}
|
||||||
|
if (searchParams?.during) {
|
||||||
|
searchOptions.during = searchParams.during;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add server-wide filter if provided
|
||||||
|
if (searchParams?.server_wide) {
|
||||||
|
searchOptions.server_wide = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await channel.searchWithUsers(searchOptions);
|
||||||
setState({ type: "results", results: data.messages });
|
setState({ type: "results", results: data.messages });
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
search();
|
search();
|
||||||
|
// Save search params when they change
|
||||||
|
if (searchParams) {
|
||||||
|
setSavedSearchParams(searchParams);
|
||||||
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [sort]);
|
}, [sort, query, searchParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericSidebarBase data-scroll-offset="with-padding">
|
<SearchSidebarBase>
|
||||||
<GenericSidebarList>
|
<GenericSidebarList>
|
||||||
<SearchBase>
|
<SearchBase>
|
||||||
<Category>
|
<Overline type="subtle" block>
|
||||||
<Error
|
|
||||||
error={<a onClick={close}>« back to members</a>}
|
|
||||||
/>
|
|
||||||
</Category>
|
|
||||||
<Category>
|
|
||||||
<Text id="app.main.channel.search.title" />
|
<Text id="app.main.channel.search.title" />
|
||||||
</Category>
|
</Overline>
|
||||||
<InputBox
|
|
||||||
value={query}
|
|
||||||
onKeyDown={(e) => e.key === "Enter" && search()}
|
|
||||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
<div className="sort">
|
<div className="sort">
|
||||||
{["Latest", "Oldest", "Relevance"].map((key) => (
|
{(["Latest", "Oldest", "Relevance"] as Sort[]).map((key) => (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
compact
|
compact
|
||||||
palette={sort === key ? "accent" : "primary"}
|
palette={sort === key ? "accent" : "secondary"}
|
||||||
onClick={() => setSort(key as Sort)}>
|
onClick={() => setSort(key)}>
|
||||||
<Text
|
<Text
|
||||||
id={`app.main.channel.search.sort.${key.toLowerCase()}`}
|
id={`app.main.channel.search.sort.${key.toLowerCase()}`}
|
||||||
/>
|
/>
|
||||||
@@ -127,30 +200,66 @@ export function SearchSidebar({ close }: Props) {
|
|||||||
{state.type === "loading" && <Preloader type="ring" />}
|
{state.type === "loading" && <Preloader type="ring" />}
|
||||||
{state.type === "results" && (
|
{state.type === "results" && (
|
||||||
<div className="list">
|
<div className="list">
|
||||||
{state.results.map((message) => {
|
{(() => {
|
||||||
let href = "";
|
// Group messages by channel
|
||||||
if (channel?.channel_type === "TextChannel") {
|
const groupedMessages = state.results.reduce((acc, message) => {
|
||||||
href += `/server/${channel.server_id}`;
|
const channelId = message.channel_id;
|
||||||
|
if (!acc[channelId]) {
|
||||||
|
acc[channelId] = [];
|
||||||
}
|
}
|
||||||
|
acc[channelId].push(message);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, MessageI[]>);
|
||||||
|
|
||||||
href += `/channel/${message.channel_id}/${message._id}`;
|
const client = useClient();
|
||||||
|
|
||||||
|
return Object.entries(groupedMessages).map(([channelId, messages]) => {
|
||||||
|
const messageChannel = client.channels.get(channelId);
|
||||||
|
const channelName = messageChannel?.name || "Unknown Channel";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={href} key={message._id}>
|
<div key={channelId}>
|
||||||
<div className="message">
|
<Overline type="subtle" block>
|
||||||
<Message
|
# {channelName}
|
||||||
message={message}
|
</Overline>
|
||||||
head
|
{messages.map((message) => {
|
||||||
hideReply
|
let href = "";
|
||||||
/>
|
if (messageChannel?.channel_type === "TextChannel") {
|
||||||
</div>
|
href += `/server/${messageChannel.server_id}`;
|
||||||
</Link>
|
}
|
||||||
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SearchBase>
|
</SearchBase>
|
||||||
</GenericSidebarList>
|
</GenericSidebarList>
|
||||||
</GenericSidebarBase>
|
</SearchSidebarBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
322
src/lib/hooks/useSearchAutoComplete.ts
Normal file
322
src/lib/hooks/useSearchAutoComplete.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { User } from "revolt.js";
|
||||||
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
|
import { AutoCompleteState, SearchClues } from "../../components/common/AutoComplete";
|
||||||
|
|
||||||
|
export interface SearchAutoCompleteProps {
|
||||||
|
state: AutoCompleteState;
|
||||||
|
setState: (state: AutoCompleteState) => void;
|
||||||
|
onKeyUp: (ev: KeyboardEvent) => void;
|
||||||
|
onKeyDown: (ev: KeyboardEvent) => boolean;
|
||||||
|
onChange: (ev: Event) => void;
|
||||||
|
onClick: (userId: string, username: string) => void;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping of user IDs to usernames for display
|
||||||
|
export interface UserMapping {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchAutoComplete(
|
||||||
|
setValue: (v: string) => void,
|
||||||
|
userMappings: UserMapping,
|
||||||
|
setUserMappings: (mappings: UserMapping) => void,
|
||||||
|
): SearchAutoCompleteProps {
|
||||||
|
const [state, setState] = useState<AutoCompleteState>({ type: "none" });
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
function findSearchPattern(
|
||||||
|
value: string,
|
||||||
|
cursorPos: number
|
||||||
|
): ["user", string, number, "from" | "mentions"] | undefined {
|
||||||
|
// Look backwards from cursor to find "from:" or "mentions:"
|
||||||
|
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*)$/);
|
||||||
|
if (fromMatch) {
|
||||||
|
// Check if there's already a username that continues after cursor
|
||||||
|
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*)$/);
|
||||||
|
if (mentionsMatch) {
|
||||||
|
// Check if there's already a username that continues after cursor
|
||||||
|
const continuationMatch = afterCursor.match(/^(\w*)/);
|
||||||
|
const fullUsername = mentionsMatch[1].replace('@', '') + (continuationMatch ? continuationMatch[1] : '');
|
||||||
|
return ["user", fullUsername, mentionsMatch.index! + 9, "mentions"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if cursor is inside an existing filter
|
||||||
|
const lastFromIndex = beforeCursor.lastIndexOf("from:");
|
||||||
|
const lastMentionsIndex = beforeCursor.lastIndexOf("mentions:");
|
||||||
|
|
||||||
|
if (lastFromIndex !== -1 || lastMentionsIndex !== -1) {
|
||||||
|
const filterIndex = Math.max(lastFromIndex, lastMentionsIndex);
|
||||||
|
const filterType = lastFromIndex > lastMentionsIndex ? "from" : "mentions";
|
||||||
|
const filterLength = filterType === "from" ? 5 : 9;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const afterFilter = value.slice(filterIndex + filterLength);
|
||||||
|
const usernameMatch = afterFilter.match(/^@?(\w*)/);
|
||||||
|
if (usernameMatch) {
|
||||||
|
return ["user", usernameMatch[1] || "", filterIndex + filterLength, filterType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange(ev: Event) {
|
||||||
|
const el = ev.target as HTMLInputElement;
|
||||||
|
const cursorPos = el.selectionStart || 0;
|
||||||
|
|
||||||
|
const result = findSearchPattern(el.value, cursorPos);
|
||||||
|
if (result) {
|
||||||
|
const [type, search, , filterType] = result;
|
||||||
|
const regex = new RegExp(search, "i");
|
||||||
|
|
||||||
|
if (type === "user") {
|
||||||
|
// Get all users - in a real app, you might want to limit this
|
||||||
|
// or use a specific search context
|
||||||
|
let users = [...client.users.values()];
|
||||||
|
|
||||||
|
// Filter out system user
|
||||||
|
users = users.filter(
|
||||||
|
(x) => x._id !== "00000000000000000000000000"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter by search term
|
||||||
|
let matches = (
|
||||||
|
search.length > 0
|
||||||
|
? users.filter((user) =>
|
||||||
|
user.username.toLowerCase().match(regex)
|
||||||
|
)
|
||||||
|
: users
|
||||||
|
)
|
||||||
|
.slice(0, 5)
|
||||||
|
.filter((x) => typeof x !== "undefined");
|
||||||
|
|
||||||
|
// Always show autocomplete when filter is typed, even with no matches
|
||||||
|
const currentPosition =
|
||||||
|
state.type !== "none" ? state.selected : 0;
|
||||||
|
|
||||||
|
setState({
|
||||||
|
type: "user",
|
||||||
|
matches,
|
||||||
|
selected: Math.min(currentPosition, matches.length - 1),
|
||||||
|
within: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.type !== "none") {
|
||||||
|
setState({ type: "none" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCurrent(el: HTMLInputElement) {
|
||||||
|
if (state.type === "user") {
|
||||||
|
const cursorPos = el.selectionStart || 0;
|
||||||
|
const result = findSearchPattern(el.value, cursorPos);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
const [, search, startIndex, filterType] = result;
|
||||||
|
const selectedUser = state.matches[state.selected];
|
||||||
|
|
||||||
|
// Store the mapping
|
||||||
|
const newMappings = { ...userMappings };
|
||||||
|
const mappingKey = `${filterType}:${selectedUser.username}`;
|
||||||
|
newMappings[mappingKey] = selectedUser._id;
|
||||||
|
setUserMappings(newMappings);
|
||||||
|
|
||||||
|
// Find the end of the current username (including @ if present)
|
||||||
|
let endIndex = startIndex;
|
||||||
|
const afterStartIndex = el.value.slice(startIndex);
|
||||||
|
const existingMatch = afterStartIndex.match(/^@?\w*/);
|
||||||
|
if (existingMatch) {
|
||||||
|
endIndex = startIndex + existingMatch[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the text with @username and add space
|
||||||
|
const before = el.value.slice(0, startIndex);
|
||||||
|
const after = el.value.slice(endIndex);
|
||||||
|
setValue(before + "@" + selectedUser.username + " " + after);
|
||||||
|
|
||||||
|
// Set cursor position after the @username and space
|
||||||
|
setTimeout(() => {
|
||||||
|
el.setSelectionRange(
|
||||||
|
startIndex + selectedUser.username.length + 2, // +1 for @, +1 for space
|
||||||
|
startIndex + selectedUser.username.length + 2
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
setState({ type: "none" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(userId: string, username: string) {
|
||||||
|
const el = document.querySelector('input[type="text"]') as HTMLInputElement;
|
||||||
|
if (el && state.type === "user") {
|
||||||
|
// Find which user was clicked
|
||||||
|
const clickedIndex = state.matches.findIndex(u => u._id === userId);
|
||||||
|
if (clickedIndex !== -1) {
|
||||||
|
setState({ ...state, selected: clickedIndex });
|
||||||
|
// Use setTimeout to ensure state is updated before selection
|
||||||
|
setTimeout(() => selectCurrent(el), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFocused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (focused && state.type !== "none") {
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.selected > 0) {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
selected: state.selected - 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.selected < state.matches.length - 1) {
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
selected: state.selected + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Enter" || e.key === "Tab") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (state.matches.length > 0) {
|
||||||
|
selectCurrent(e.currentTarget as HTMLInputElement);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
setState({ type: "none" });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
|
if (e.currentTarget !== null) {
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocus() {
|
||||||
|
setFocused(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlur() {
|
||||||
|
if (state.type !== "none" && state.within) return;
|
||||||
|
setFocused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: focused ? state : { type: "none" },
|
||||||
|
setState,
|
||||||
|
onClick,
|
||||||
|
onChange,
|
||||||
|
onKeyUp,
|
||||||
|
onKeyDown,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform display query with usernames to API query with user IDs
|
||||||
|
export function transformSearchQuery(
|
||||||
|
displayQuery: string,
|
||||||
|
userMappings: UserMapping
|
||||||
|
): { query: string; author?: string; mention?: string; before_date?: string; after_date?: string; during?: 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 server_wide: boolean | undefined;
|
||||||
|
|
||||||
|
// Extract and replace from:@username with user ID
|
||||||
|
const fromMatch = query.match(/\bfrom:@?(\w+)/);
|
||||||
|
if (fromMatch) {
|
||||||
|
const username = fromMatch[1];
|
||||||
|
const userId = userMappings[`from:${username}`];
|
||||||
|
if (userId) {
|
||||||
|
author = userId;
|
||||||
|
// Remove from:@username from query
|
||||||
|
query = query.replace(fromMatch[0], "").trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and replace mentions:@username with user ID
|
||||||
|
const mentionsMatch = query.match(/\bmentions:@?(\w+)/);
|
||||||
|
if (mentionsMatch) {
|
||||||
|
const username = mentionsMatch[1];
|
||||||
|
const userId = userMappings[`mentions:${username}`];
|
||||||
|
if (userId) {
|
||||||
|
mention = userId;
|
||||||
|
// Remove mentions:@username from query
|
||||||
|
query = query.replace(mentionsMatch[0], "").trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract date filters (YYYY-MM-DD format) and convert to ISO 8601
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
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();
|
||||||
|
query = query.replace(duringMatch[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 };
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import { Search } from "@styled-icons/boxicons-regular";
|
|
||||||
import {
|
import {
|
||||||
UserPlus,
|
UserPlus,
|
||||||
Cog,
|
Cog,
|
||||||
@@ -23,50 +22,12 @@ import { SIDEBAR_MEMBERS } from "../../../mobx/stores/Layout";
|
|||||||
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
import UpdateIndicator from "../../../components/common/UpdateIndicator";
|
||||||
import { modalController } from "../../../controllers/modals/ModalController";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import { ChannelHeaderProps } from "../ChannelHeader";
|
import { ChannelHeaderProps } from "../ChannelHeader";
|
||||||
|
import { SearchBar } from "../../../components/navigation/SearchBar";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
`;
|
|
||||||
|
|
||||||
const SearchBar = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--primary-background);
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
width: 120px;
|
|
||||||
transition: width .25s ease;
|
|
||||||
|
|
||||||
:focus-within {
|
|
||||||
width: 200px;
|
|
||||||
box-shadow: 0 0 0 1pt var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
all: unset;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-weight: 400;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
background: inherit;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
opacity: 0.4;
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
||||||
@@ -138,19 +99,7 @@ export default function HeaderActions({ channel }: ChannelHeaderProps) {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
{channel.channel_type !== "VoiceChannel" && (
|
{channel.channel_type !== "VoiceChannel" && (
|
||||||
/*<SearchBar>
|
<SearchBar />
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
onClick={openSearch}
|
|
||||||
/>
|
|
||||||
<div className="actions">
|
|
||||||
<Search size={18} />
|
|
||||||
</div>
|
|
||||||
</SearchBar>*/
|
|
||||||
<IconButton onClick={openSearch}>
|
|
||||||
<Search size={25} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user