Merge pull request #50 from archem-team/feature/ghost-ping

Feature/ghost ping
This commit is contained in:
Abron Studio
2025-03-10 12:47:42 +03:30
committed by GitHub
4 changed files with 81 additions and 40 deletions

View File

@@ -17,19 +17,19 @@ import UserIcon from "./user/UserIcon";
export type AutoCompleteState = export type AutoCompleteState =
| { type: "none" } | { type: "none" }
| ({ selected: number; within: boolean } & ( | ({ selected: number; within: boolean } & (
| { | {
type: "emoji"; type: "emoji";
matches: (string | CustomEmoji)[]; matches: (string | CustomEmoji)[];
} }
| { | {
type: "user"; type: "user";
matches: User[]; matches: User[];
} }
| { | {
type: "channel"; type: "channel";
matches: Channel[]; matches: Channel[];
} }
)); ));
export type SearchClues = { export type SearchClues = {
users?: { type: "channel"; id: string } | { type: "all" }; users?: { type: "channel"; id: string } | { type: "all" };
@@ -89,8 +89,8 @@ export function useAutoComplete(
current === "#" current === "#"
? "channel" ? "channel"
: current === ":" : current === ":"
? "emoji" ? "emoji"
: "user", : "user",
search.toLowerCase(), search.toLowerCase(),
current === ":" ? j + 1 : j, current === ":" ? j + 1 : j,
]; ];
@@ -177,8 +177,8 @@ export function useAutoComplete(
const matches = ( const matches = (
search.length > 0 search.length > 0
? users.filter((user) => ? users.filter((user) =>
user.username.toLowerCase().match(regex), user.username.toLowerCase().match(regex),
) )
: users : users
) )
.splice(0, 5) .splice(0, 5)
@@ -209,8 +209,8 @@ export function useAutoComplete(
const matches = ( const matches = (
search.length > 0 search.length > 0
? channels.filter((channel) => ? channels.filter((channel) =>
channel.name!.toLowerCase().match(regex), channel.name!.toLowerCase().match(regex),
) )
: channels : channels
) )
.splice(0, 5) .splice(0, 5)
@@ -255,12 +255,13 @@ export function useAutoComplete(
": ", ": ",
); );
} else if (state.type === "user") { } else if (state.type === "user") {
const selectedUser = state.matches[state.selected];
content.splice( content.splice(
index, index,
search.length + 1, search.length + 1,
"<@", "@",
state.matches[state.selected]._id, selectedUser.username,
"> ", " ",
); );
} else { } else {
content.splice( content.splice(
@@ -460,11 +461,10 @@ export default function AutoComplete({
size={20} size={20}
/> />
)} )}
<span style={{ paddingLeft: "4px" }}>{`:${ <span style={{ paddingLeft: "4px" }}>{`:${match instanceof CustomEmoji
match instanceof CustomEmoji
? match.name ? match.name
: match : match
}:`}</span> }:`}</span>
</div> </div>
{match instanceof CustomEmoji && {match instanceof CustomEmoji &&
match.parent.type == "Server" && ( match.parent.type == "Server" && (

View File

@@ -13,7 +13,7 @@ import { IconButton, Picker } from "@revoltchat/ui";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize"; import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { debounce } from "../../../lib/debounce"; import { debounce } from "../../../lib/debounce";
import { defer } from "../../../lib/defer"; import { defer, chainedDefer } from "../../../lib/defer";
import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter"; import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
import { useTranslation } from "../../../lib/i18n"; import { useTranslation } from "../../../lib/i18n";
import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice";
@@ -325,10 +325,29 @@ export default observer(({ channel }: Props) => {
if (uploadState.type === "uploading" || uploadState.type === "sending") if (uploadState.type === "uploading" || uploadState.type === "sending")
return; 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 (uploadState.type !== "none") return sendFile(content);
if (content.length === 0) return; 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"); internalEmit("NewMessages", "hide");
stopTyping(); stopTyping();
setMessage(); setMessage();
@@ -366,7 +385,7 @@ export default observer(({ channel }: Props) => {
content: newContent.substr(0, 2000), content: newContent.substr(0, 2000),
}) })
.then(() => .then(() =>
defer(() => chainedDefer(() =>
renderer.jumpToBottom( renderer.jumpToBottom(
SMOOTH_SCROLL_ON_RECEIVE, SMOOTH_SCROLL_ON_RECEIVE,
), ),
@@ -388,7 +407,8 @@ export default observer(({ channel }: Props) => {
replies, replies,
}); });
defer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE)); // Use chainedDefer for more reliable scrolling
chainedDefer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
try { try {
await channel.sendMessage({ await channel.sendMessage({
@@ -396,6 +416,9 @@ export default observer(({ channel }: Props) => {
nonce, nonce,
replies, replies,
}); });
// Add another scroll to bottom after the message is sent
chainedDefer(() => renderer.jumpToBottom(SMOOTH_SCROLL_ON_RECEIVE));
} catch (error) { } catch (error) {
state.queue.fail(nonce, takeError(error)); state.queue.fail(nonce, takeError(error));
} }

View File

@@ -88,8 +88,12 @@ const Divider = styled.div`
export const MessageOverlayBar = observer( export const MessageOverlayBar = observer(
({ reactionsOpen, setReactionsOpen, message, queued }: Props) => { ({ reactionsOpen, setReactionsOpen, message, queued }: Props) => {
if (!message) {
return null;
}
const client = message.client; 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 [copied, setCopied] = useState<"link" | "id">(null!);
const [extraActions, setExtra] = useState(shiftKeyPressed); const [extraActions, setExtra] = useState(shiftKeyPressed);
@@ -147,17 +151,17 @@ export const MessageOverlayBar = observer(
</Tooltip> </Tooltip>
)} )}
{isAuthor || {isAuthor ||
(message.channel && (message.channel &&
message.channel.havePermission("ManageMessages")) ? ( message.channel.havePermission("ManageMessages")) ? (
<Tooltip content="Delete"> <Tooltip content="Delete">
<Entry <Entry
onClick={(e) => onClick={(e) =>
e.shiftKey e.shiftKey
? message.delete() ? message.delete()
: modalController.push({ : modalController.push({
type: "delete_message", type: "delete_message",
target: message, target: message,
}) })
}> }>
<Trash size={18} color={"var(--error)"} /> <Trash size={18} color={"var(--error)"} />
</Entry> </Entry>

View File

@@ -19,6 +19,7 @@ import Tooltip from "../../common/Tooltip";
import UserIcon from "../../common/user/UserIcon"; import UserIcon from "../../common/user/UserIcon";
import { Username } from "../../common/user/UserShort"; import { Username } from "../../common/user/UserShort";
import UserStatus from "../../common/user/UserStatus"; import UserStatus from "../../common/user/UserStatus";
import { useClient } from "../../../controllers/client/ClientController";
type CommonProps = Omit< type CommonProps = Omit<
JSX.HTMLAttributes<HTMLDivElement>, JSX.HTMLAttributes<HTMLDivElement>,
@@ -37,6 +38,15 @@ type UserProps = CommonProps & {
channel?: Channel; 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. // TODO: Gray out blocked names.
export const UserButton = observer((props: UserProps) => { export const UserButton = observer((props: UserProps) => {
const { const {
@@ -50,6 +60,8 @@ export const UserButton = observer((props: UserProps) => {
...divProps ...divProps
} = props; } = props;
const client = useClient();
return ( return (
<div <div
{...divProps} {...divProps}
@@ -81,8 +93,8 @@ export const UserButton = observer((props: UserProps) => {
{ {
<div className={styles.subText}> <div className={styles.subText}>
{typeof channel?.last_message?.content === "string" && {typeof channel?.last_message?.content === "string" &&
alert ? ( alert ? (
channel.last_message.content.slice(0, 32) convertMentionsToUsernames(channel.last_message.content, client).slice(0, 32)
) : ( ) : (
<UserStatus user={user} tooltip /> <UserStatus user={user} tooltip />
)} )}
@@ -140,6 +152,8 @@ export const ChannelButton = observer((props: ChannelProps) => {
...divProps ...divProps
} = props; } = props;
const client = useClient();
if (channel.channel_type === "SavedMessages") throw "Invalid channel type."; if (channel.channel_type === "SavedMessages") throw "Invalid channel type.";
if (channel.channel_type === "DirectMessage") { if (channel.channel_type === "DirectMessage") {
if (typeof user === "undefined") throw "No user provided."; if (typeof user === "undefined") throw "No user provided.";
@@ -170,9 +184,9 @@ export const ChannelButton = observer((props: ChannelProps) => {
{channel.channel_type === "Group" && ( {channel.channel_type === "Group" && (
<div className={styles.subText}> <div className={styles.subText}>
{typeof channel.last_message?.content === "string" && {typeof channel.last_message?.content === "string" &&
alert && alert &&
!muted ? ( !muted ? (
channel.last_message.content.slice(0, 32) convertMentionsToUsernames(channel.last_message.content, client).slice(0, 32)
) : ( ) : (
<Text <Text
id="quantities.members" id="quantities.members"