Add channel auto-complete.

This commit is contained in:
Paul 2021-06-22 13:35:43 +01:00
parent f724cfdf4f
commit feaec3f8d9
2 changed files with 97 additions and 19 deletions

View file

@ -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>
) )

View file

@ -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 (
<> <>