From 20a9573440f375f0959c49df34db9d03dfabe48b Mon Sep 17 00:00:00 2001 From: Abron Date: Thu, 6 Mar 2025 12:48:21 +0330 Subject: [PATCH 1/2] mentions in chat --- src/components/common/AutoComplete.tsx | 50 +++++++++---------- .../common/messaging/MessageBox.tsx | 31 ++++++++++-- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/components/common/AutoComplete.tsx b/src/components/common/AutoComplete.tsx index 72994842..a44e047b 100644 --- a/src/components/common/AutoComplete.tsx +++ b/src/components/common/AutoComplete.tsx @@ -17,19 +17,19 @@ import UserIcon from "./user/UserIcon"; export type AutoCompleteState = | { type: "none" } | ({ selected: number; within: boolean } & ( - | { - type: "emoji"; - matches: (string | CustomEmoji)[]; - } - | { - type: "user"; - matches: User[]; - } - | { - type: "channel"; - matches: Channel[]; - } - )); + | { + type: "emoji"; + matches: (string | CustomEmoji)[]; + } + | { + type: "user"; + matches: User[]; + } + | { + type: "channel"; + matches: Channel[]; + } + )); export type SearchClues = { users?: { type: "channel"; id: string } | { type: "all" }; @@ -89,8 +89,8 @@ export function useAutoComplete( current === "#" ? "channel" : current === ":" - ? "emoji" - : "user", + ? "emoji" + : "user", search.toLowerCase(), current === ":" ? j + 1 : j, ]; @@ -177,8 +177,8 @@ export function useAutoComplete( const matches = ( search.length > 0 ? users.filter((user) => - user.username.toLowerCase().match(regex), - ) + user.username.toLowerCase().match(regex), + ) : users ) .splice(0, 5) @@ -209,8 +209,8 @@ export function useAutoComplete( const matches = ( search.length > 0 ? channels.filter((channel) => - channel.name!.toLowerCase().match(regex), - ) + channel.name!.toLowerCase().match(regex), + ) : channels ) .splice(0, 5) @@ -255,12 +255,13 @@ export function useAutoComplete( ": ", ); } else if (state.type === "user") { + const selectedUser = state.matches[state.selected]; content.splice( index, search.length + 1, - "<@", - state.matches[state.selected]._id, - "> ", + "@", + selectedUser.username, + " ", ); } else { content.splice( @@ -460,11 +461,10 @@ export default function AutoComplete({ size={20} /> )} - {`:${ - match instanceof CustomEmoji + {`:${match instanceof CustomEmoji ? match.name : match - }:`} + }:`} {match instanceof CustomEmoji && match.parent.type == "Server" && ( diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index c38ed11f..2e7294e0 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -13,7 +13,7 @@ import { IconButton, Picker } from "@revoltchat/ui"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import { debounce } from "../../../lib/debounce"; -import { defer } from "../../../lib/defer"; +import { defer, chainedDefer } from "../../../lib/defer"; import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter"; import { useTranslation } from "../../../lib/i18n"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; @@ -325,10 +325,29 @@ export default observer(({ channel }: Props) => { if (uploadState.type === "uploading" || uploadState.type === "sending") return; - const content = state.draft.get(channel._id)?.content?.trim() ?? ""; + let content = state.draft.get(channel._id)?.content?.trim() ?? ""; if (uploadState.type !== "none") return sendFile(content); if (content.length === 0) return; + // Convert @username mentions to <@USER_ID> format + const mentionRegex = /@([a-zA-Z0-9_]+)/g; + const mentionMatches = content.match(mentionRegex); + + if (mentionMatches) { + for (const mention of mentionMatches) { + const username = mention.substring(1); // Remove the @ symbol + // Find the user with this username + const user = Array.from(client.users.values()).find( + (u) => u.username.toLowerCase() === username.toLowerCase() + ); + + if (user) { + // Replace @username with <@USER_ID> + content = content.replace(mention, `<@${user._id}>`); + } + } + } + internalEmit("NewMessages", "hide"); stopTyping(); setMessage(); @@ -366,7 +385,7 @@ export default observer(({ channel }: Props) => { content: newContent.substr(0, 2000), }) .then(() => - defer(() => + chainedDefer(() => renderer.jumpToBottom( SMOOTH_SCROLL_ON_RECEIVE, ), @@ -388,7 +407,8 @@ export default observer(({ channel }: Props) => { replies, }); - defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); + // Use chainedDefer for more reliable scrolling + chainedDefer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); try { await channel.sendMessage({ @@ -396,6 +416,9 @@ export default observer(({ channel }: Props) => { nonce, replies, }); + + // Add another scroll to bottom after the message is sent + chainedDefer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); } catch (error) { state.queue.fail(nonce, takeError(error)); } From 7143a46a6ec9739971e0eeb4bf6a1c5b8d11a081 Mon Sep 17 00:00:00 2001 From: Abron Date: Thu, 6 Mar 2025 13:18:06 +0330 Subject: [PATCH 2/2] conversation list --- .../messaging/bars/MessageOverlayBar.tsx | 16 ++++++++----- .../navigation/items/ButtonItem.tsx | 24 +++++++++++++++---- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/common/messaging/bars/MessageOverlayBar.tsx b/src/components/common/messaging/bars/MessageOverlayBar.tsx index 29735102..492e283b 100644 --- a/src/components/common/messaging/bars/MessageOverlayBar.tsx +++ b/src/components/common/messaging/bars/MessageOverlayBar.tsx @@ -88,8 +88,12 @@ const Divider = styled.div` export const MessageOverlayBar = observer( ({ reactionsOpen, setReactionsOpen, message, queued }: Props) => { + if (!message) { + return null; + } + const client = message.client; - const isAuthor = message.author_id === client.user!._id; + const isAuthor = message.author_id === client.user?._id; const [copied, setCopied] = useState<"link" | "id">(null!); const [extraActions, setExtra] = useState(shiftKeyPressed); @@ -147,17 +151,17 @@ export const MessageOverlayBar = observer( )} {isAuthor || - (message.channel && - message.channel.havePermission("ManageMessages")) ? ( + (message.channel && + message.channel.havePermission("ManageMessages")) ? ( e.shiftKey ? message.delete() : modalController.push({ - type: "delete_message", - target: message, - }) + type: "delete_message", + target: message, + }) }> diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx index 77d7e68f..b98b421f 100644 --- a/src/components/navigation/items/ButtonItem.tsx +++ b/src/components/navigation/items/ButtonItem.tsx @@ -19,6 +19,7 @@ import Tooltip from "../../common/Tooltip"; import UserIcon from "../../common/user/UserIcon"; import { Username } from "../../common/user/UserShort"; import UserStatus from "../../common/user/UserStatus"; +import { useClient } from "../../../controllers/client/ClientController"; type CommonProps = Omit< JSX.HTMLAttributes, @@ -37,6 +38,15 @@ type UserProps = CommonProps & { channel?: Channel; }; +// Helper function to convert mentions to usernames +function convertMentionsToUsernames(content: string, client: any): string { + const mentionRegex = /<@([A-z0-9]{26})>/g; + return content.replace(mentionRegex, (match, userId) => { + const user = client.users.get(userId); + return user ? `@${user.username}` : match; + }); +} + // TODO: Gray out blocked names. export const UserButton = observer((props: UserProps) => { const { @@ -50,6 +60,8 @@ export const UserButton = observer((props: UserProps) => { ...divProps } = props; + const client = useClient(); + return (
{ {
{typeof channel?.last_message?.content === "string" && - alert ? ( - channel.last_message.content.slice(0, 32) + alert ? ( + convertMentionsToUsernames(channel.last_message.content, client).slice(0, 32) ) : ( )} @@ -140,6 +152,8 @@ export const ChannelButton = observer((props: ChannelProps) => { ...divProps } = props; + const client = useClient(); + if (channel.channel_type === "SavedMessages") throw "Invalid channel type."; if (channel.channel_type === "DirectMessage") { if (typeof user === "undefined") throw "No user provided."; @@ -170,9 +184,9 @@ export const ChannelButton = observer((props: ChannelProps) => { {channel.channel_type === "Group" && (
{typeof channel.last_message?.content === "string" && - alert && - !muted ? ( - channel.last_message.content.slice(0, 32) + alert && + !muted ? ( + convertMentionsToUsernames(channel.last_message.content, client).slice(0, 32) ) : (