From feaec3f8d9139c7004019931198459cd3597e41e Mon Sep 17 00:00:00 2001 From: Paul Date: Tue, 22 Jun 2021 13:35:43 +0100 Subject: [PATCH] Add channel auto-complete. --- src/components/common/AutoComplete.tsx | 110 +++++++++++++++--- .../common/messaging/MessageBox.tsx | 6 +- 2 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx index 5908dcb6..50adce4e 100644 --- a/src/components/common/AutoComplete.tsx +++ b/src/components/common/AutoComplete.tsx @@ -1,28 +1,33 @@ import { StateUpdater, useContext, useState } from "preact/hooks"; import { AppContext } from "../../context/revoltjs/RevoltClient"; +import { Channels } from "revolt.js/dist/api/objects"; import { emojiDictionary } from "../../assets/emojis"; +import { SYSTEM_USER_ID, User } from "revolt.js"; import UserIcon from "./user/UserIcon"; import styled from "styled-components"; -import { SYSTEM_USER_ID, User } from "revolt.js"; import Emoji from "./Emoji"; +import ChannelIcon from "./ChannelIcon"; export type AutoCompleteState = | { type: "none" } - | { - type: "emoji"; - matches: string[]; - selected: number; - within: boolean; - } - | { - type: "user"; - matches: User[]; - selected: number; - within: boolean; - }; + | ({ selected: number; within: boolean; } & ( + { + type: "emoji"; + matches: string[]; + } | + { + type: "user"; + matches: User[]; + } | + { + type: "channel"; + matches: Channels.TextChannel[]; + } + )); export type SearchClues = { - users?: { type: 'channel', id: string } | { type: 'all' } + users?: { type: 'channel', id: string } | { type: 'all' }, + channels?: { server: string } }; export type AutoCompleteProps = { @@ -44,7 +49,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se function findSearchString( el: HTMLTextAreaElement - ): ["emoji" | "user", string, number] | undefined { + ): ["emoji" | "user" | "channel", string, number] | undefined { if (el.selectionStart === el.selectionEnd) { let cursor = el.selectionStart; let content = el.value.slice(0, cursor); @@ -58,6 +63,12 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se "", j ]; + } else if (content[j] === '#') { + return [ + "channel", + "", + 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; let current = content[j]; - if (current === ":" || current === "@") { + if (current === ":" || current === "@" || current === "#") { let search = content.slice(j + 1, content.length); if (search.length > 0) { return [ + current === "#" ? "channel" : current === ":" ? "emoji" : "user", search.toLowerCase(), j + 1 @@ -137,7 +149,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se 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) .filter(x => typeof x !== "undefined"); @@ -157,6 +169,33 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se 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") { @@ -178,7 +217,7 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se state.matches[state.selected], ": " ); - } else { + } else if (state.type === 'user') { content.splice( index - 1, search.length + 1, @@ -186,6 +225,14 @@ export function useAutoComplete(setValue: (v?: string) => void, searchClues?: Se state.matches[state.selected]._id, "> " ); + } else { + content.splice( + index - 1, + search.length + 1, + "<#", + state.matches[state.selected]._id, + "> " + ); } setValue(content.join("")); @@ -358,6 +405,33 @@ export default function AutoComplete({ state, setState, onClick }: Pick ))} + {state.type === "channel" && + state.matches.map((match, i) => ( + + ))} ) diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 4260bdea..491b4b67 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -227,7 +227,11 @@ function MessageBox({ channel, draft, dispatcher }: Props) { } 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 ( <>