mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-25 23:22:06 -05:00
Add channel auto-complete.
This commit is contained in:
parent
f724cfdf4f
commit
feaec3f8d9
2 changed files with 97 additions and 19 deletions
|
@ -1,28 +1,33 @@
|
||||||
import { StateUpdater, useContext, useState } from "preact/hooks";
|
import { StateUpdater, useContext, useState } from "preact/hooks";
|
||||||
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
import { AppContext } from "../../context/revoltjs/RevoltClient";
|
||||||
|
import { Channels } from "revolt.js/dist/api/objects";
|
||||||
import { emojiDictionary } from "../../assets/emojis";
|
import { emojiDictionary } from "../../assets/emojis";
|
||||||
|
import { SYSTEM_USER_ID, User } from "revolt.js";
|
||||||
import UserIcon from "./user/UserIcon";
|
import UserIcon from "./user/UserIcon";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { SYSTEM_USER_ID, User } from "revolt.js";
|
|
||||||
import Emoji from "./Emoji";
|
import Emoji from "./Emoji";
|
||||||
|
import ChannelIcon from "./ChannelIcon";
|
||||||
|
|
||||||
export type AutoCompleteState =
|
export type AutoCompleteState =
|
||||||
| { type: "none" }
|
| { type: "none" }
|
||||||
| {
|
| ({ selected: number; within: boolean; } & (
|
||||||
|
{
|
||||||
type: "emoji";
|
type: "emoji";
|
||||||
matches: string[];
|
matches: string[];
|
||||||
selected: number;
|
} |
|
||||||
within: boolean;
|
{
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "user";
|
type: "user";
|
||||||
matches: User[];
|
matches: User[];
|
||||||
selected: number;
|
} |
|
||||||
within: boolean;
|
{
|
||||||
};
|
type: "channel";
|
||||||
|
matches: Channels.TextChannel[];
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
export type SearchClues = {
|
export type SearchClues = {
|
||||||
users?: { type: 'channel', id: string } | { type: 'all' }
|
users?: { type: 'channel', id: string } | { type: 'all' },
|
||||||
|
channels?: { server: string }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AutoCompleteProps = {
|
export type AutoCompleteProps = {
|
||||||
|
@ -44,7 +49,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
|
|
||||||
function findSearchString(
|
function findSearchString(
|
||||||
el: HTMLTextAreaElement
|
el: HTMLTextAreaElement
|
||||||
): ["emoji" | "user", string, number] | undefined {
|
): ["emoji" | "user" | "channel", string, number] | undefined {
|
||||||
if (el.selectionStart === el.selectionEnd) {
|
if (el.selectionStart === el.selectionEnd) {
|
||||||
let cursor = el.selectionStart;
|
let cursor = el.selectionStart;
|
||||||
let content = el.value.slice(0, cursor);
|
let content = el.value.slice(0, cursor);
|
||||||
|
@ -58,6 +63,12 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
"",
|
"",
|
||||||
j
|
j
|
||||||
];
|
];
|
||||||
|
} else if (content[j] === '#') {
|
||||||
|
return [
|
||||||
|
"channel",
|
||||||
|
"",
|
||||||
|
j
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
while (j >= 0 && valid.test(content[j])) {
|
while (j >= 0 && valid.test(content[j])) {
|
||||||
|
@ -67,10 +78,11 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
if (j === -1) return;
|
if (j === -1) return;
|
||||||
let current = content[j];
|
let current = content[j];
|
||||||
|
|
||||||
if (current === ":" || current === "@") {
|
if (current === ":" || current === "@" || current === "#") {
|
||||||
let search = content.slice(j + 1, content.length);
|
let search = content.slice(j + 1, content.length);
|
||||||
if (search.length > 0) {
|
if (search.length > 0) {
|
||||||
return [
|
return [
|
||||||
|
current === "#" ? "channel" :
|
||||||
current === ":" ? "emoji" : "user",
|
current === ":" ? "emoji" : "user",
|
||||||
search.toLowerCase(),
|
search.toLowerCase(),
|
||||||
j + 1
|
j + 1
|
||||||
|
@ -137,7 +149,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
|
|
||||||
users = users.filter(x => x._id !== SYSTEM_USER_ID);
|
users = users.filter(x => x._id !== SYSTEM_USER_ID);
|
||||||
|
|
||||||
let matches = (search.length > 0 ? users.filter(user => user?.username.toLowerCase().match(regex)) : users)
|
let matches = (search.length > 0 ? users.filter(user => user.username.toLowerCase().match(regex)) : users)
|
||||||
.splice(0, 5)
|
.splice(0, 5)
|
||||||
.filter(x => typeof x !== "undefined");
|
.filter(x => typeof x !== "undefined");
|
||||||
|
|
||||||
|
@ -157,6 +169,33 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'channel' && searchClues?.channels) {
|
||||||
|
let channels = client.servers.get(searchClues.channels.server)
|
||||||
|
?.channels
|
||||||
|
.map(x => client.channels.get(x))
|
||||||
|
.filter(x => typeof x !== 'undefined') as Channels.TextChannel[];
|
||||||
|
|
||||||
|
let matches = (search.length > 0 ? channels.filter(channel => channel.name.toLowerCase().match(regex)) : channels)
|
||||||
|
.splice(0, 5)
|
||||||
|
.filter(x => typeof x !== "undefined");
|
||||||
|
|
||||||
|
if (matches.length > 0) {
|
||||||
|
let currentPosition =
|
||||||
|
state.type !== "none"
|
||||||
|
? state.selected
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
setState({
|
||||||
|
type: "channel",
|
||||||
|
matches,
|
||||||
|
selected: Math.min(currentPosition, matches.length - 1),
|
||||||
|
within: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.type !== "none") {
|
if (state.type !== "none") {
|
||||||
|
@ -178,7 +217,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
state.matches[state.selected],
|
state.matches[state.selected],
|
||||||
": "
|
": "
|
||||||
);
|
);
|
||||||
} else {
|
} else if (state.type === 'user') {
|
||||||
content.splice(
|
content.splice(
|
||||||
index - 1,
|
index - 1,
|
||||||
search.length + 1,
|
search.length + 1,
|
||||||
|
@ -186,6 +225,14 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se
|
||||||
state.matches[state.selected]._id,
|
state.matches[state.selected]._id,
|
||||||
"> "
|
"> "
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
content.splice(
|
||||||
|
index - 1,
|
||||||
|
search.length + 1,
|
||||||
|
"<#",
|
||||||
|
state.matches[state.selected]._id,
|
||||||
|
"> "
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(content.join(""));
|
setValue(content.join(""));
|
||||||
|
@ -358,6 +405,33 @@ export default function AutoComplete({ state, setState, onClick }: Pick<AutoComp
|
||||||
{match.username}
|
{match.username}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{state.type === "channel" &&
|
||||||
|
state.matches.map((match, i) => (
|
||||||
|
<button
|
||||||
|
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={onClick}>
|
||||||
|
<ChannelIcon
|
||||||
|
size={24}
|
||||||
|
target={match} />
|
||||||
|
{match.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Base>
|
</Base>
|
||||||
)
|
)
|
||||||
|
|
|
@ -227,7 +227,11 @@ function MessageBox({ channel, draft, dispatcher }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ channel._id ]);
|
const debouncedStopTyping = useCallback(debounce(stopTyping, 1000), [ channel._id ]);
|
||||||
const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } = useAutoComplete(setMessage, { users: { type: 'channel', id: channel._id } });
|
const { onChange, onKeyUp, onKeyDown, onFocus, onBlur, ...autoCompleteProps } =
|
||||||
|
useAutoComplete(setMessage, {
|
||||||
|
users: { type: 'channel', id: channel._id },
|
||||||
|
channels: channel.channel_type === 'TextChannel' ? { server: channel.server } : undefined
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in a new issue