commit
27db5ea839
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
<span style={{ paddingLeft: "4px" }}>{`:${
|
||||
match instanceof CustomEmoji
|
||||
<span style={{ paddingLeft: "4px" }}>{`:${match instanceof CustomEmoji
|
||||
? match.name
|
||||
: match
|
||||
}:`}</span>
|
||||
}:`}</span>
|
||||
</div>
|
||||
{match instanceof CustomEmoji &&
|
||||
match.parent.type == "Server" && (
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
</Tooltip>
|
||||
)}
|
||||
{isAuthor ||
|
||||
(message.channel &&
|
||||
message.channel.havePermission("ManageMessages")) ? (
|
||||
(message.channel &&
|
||||
message.channel.havePermission("ManageMessages")) ? (
|
||||
<Tooltip content="Delete">
|
||||
<Entry
|
||||
onClick={(e) =>
|
||||
e.shiftKey
|
||||
? message.delete()
|
||||
: modalController.push({
|
||||
type: "delete_message",
|
||||
target: message,
|
||||
})
|
||||
type: "delete_message",
|
||||
target: message,
|
||||
})
|
||||
}>
|
||||
<Trash size={18} color={"var(--error)"} />
|
||||
</Entry>
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>,
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
{...divProps}
|
||||
|
|
@ -81,8 +93,8 @@ export const UserButton = observer((props: UserProps) => {
|
|||
{
|
||||
<div className={styles.subText}>
|
||||
{typeof channel?.last_message?.content === "string" &&
|
||||
alert ? (
|
||||
channel.last_message.content.slice(0, 32)
|
||||
alert ? (
|
||||
convertMentionsToUsernames(channel.last_message.content, client).slice(0, 32)
|
||||
) : (
|
||||
<UserStatus user={user} tooltip />
|
||||
)}
|
||||
|
|
@ -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" && (
|
||||
<div className={styles.subText}>
|
||||
{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)
|
||||
) : (
|
||||
<Text
|
||||
id="quantities.members"
|
||||
|
|
|
|||
Loading…
Reference in New Issue