diff --git a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx index 84d56bc6d..3a6d69bdd 100644 --- a/src/app/components/invite-user-prompt/InviteUserPrompt.tsx +++ b/src/app/components/invite-user-prompt/InviteUserPrompt.tsx @@ -34,7 +34,13 @@ import { isKeyHotkey } from 'is-hotkey'; import FocusTrap from 'focus-trap-react'; import { stopPropagation } from '../../utils/keyboard'; import { useDirectUsers } from '../../hooks/useDirectUsers'; -import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../utils/matrix'; +import { + getCanonicalAliasOrRoomId, + getMxIdLocalPart, + getMxIdServer, + isRoomAlias, + isUserId, +} from '../../utils/matrix'; import { Membership } from '../../../types/matrix/room'; import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch'; import { highlightText, makeHighlightRegex } from '../../plugins/react-custom-html-parser'; @@ -42,6 +48,9 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { BreakWord } from '../../styles/Text.css'; import { useAlive } from '../../hooks/useAlive'; +import { copyToClipboard } from '../../utils/dom'; +import { getMatrixToRoom } from '../../plugins/matrix-to'; +import { getViaServers } from '../../plugins/via-servers'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, @@ -58,6 +67,15 @@ type InviteUserProps = { export function InviteUserPrompt({ room, requestClose }: InviteUserProps) { const mx = useMatrixClient(); const alive = useAlive(); + const [linkCopied, setLinkCopied] = useState(false); + + const handleCopyLink = () => { + const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); + const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); + copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers)); + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), 2000); + }; const inputRef = useRef(null); const directUsers = useDirectUsers(); @@ -172,7 +190,18 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) { Invite - + + diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index ee283fcd4..acaceb1f4 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -4,6 +4,7 @@ import { Button, Chip, color, + config, Icon, Icons, Input, @@ -11,7 +12,7 @@ import { Text, TextArea, } from 'folds'; -import React, { FormEventHandler, useCallback, useMemo, useState } from 'react'; +import React, { FormEventHandler, useCallback, useMemo, useRef, useState } from 'react'; import { useAtomValue } from 'jotai'; import Linkify from 'linkify-react'; import classNames from 'classnames'; @@ -43,6 +44,22 @@ import { RoomPermissionsAPI } from '../../../hooks/useRoomPermissions'; const MARKDOWN_PATTERN = /(\*\*|__|\*|_|~~|`|\[.+?\]\(.+?\))/; +function wrapSelection(textarea: HTMLTextAreaElement, syntax: string, placeholder: string) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selected = textarea.value.substring(start, end); + const inner = selected || placeholder; + const replacement = `${syntax}${inner}${syntax}`; + const newValue = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); + const nativeSetter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set; + nativeSetter?.call(textarea, newValue); + textarea.dispatchEvent(new Event('input', { bubbles: true })); + const cursorStart = start + syntax.length; + const cursorEnd = cursorStart + inner.length; + textarea.focus(); + textarea.setSelectionRange(cursorStart, cursorEnd); +} + function topicMarkdownToHtml(text: string): string { return text .replace(/&/g, '&') @@ -60,11 +77,11 @@ function topicMarkdownToHtml(text: string): string { function buildTopicContent(topic: string): Record { if (!MARKDOWN_PATTERN.test(topic)) return { topic }; - return { - topic, - format: 'org.matrix.custom.html', - formatted_body: topicMarkdownToHtml(topic), - }; + const formattedBody = topicMarkdownToHtml(topic); + // Use HTML-stripped text as the plain topic so the header shows clean text, not raw markdown syntax + const plainTopic = formattedBody.replace(/
/g, '\n').replace(/<[^>]+>/g, ''); + // eslint-disable-next-line @typescript-eslint/naming-convention + return { topic: plainTopic, format: 'org.matrix.custom.html', formatted_body: formattedBody }; } type RoomProfileEditProps = { @@ -96,6 +113,7 @@ export function RoomProfileEdit({ ? (mxcUrlToHttp(mx, roomAvatar, useAuthentication) ?? undefined) : undefined; + const topicRef = useRef(null); const [imageFile, setImageFile] = useState(); const avatarFileUrl = useObjectURL(imageFile); const uploadingAvatar = avatarFileUrl ? roomAvatar === avatar : false; @@ -247,20 +265,60 @@ export function RoomProfileEdit({ />
- Topic + + Topic + {canEditTopic && !submitting && ( + + {( + [ + { label: 'B', syntax: '**', placeholder: 'bold', title: 'Bold' }, + { label: 'I', syntax: '*', placeholder: 'italic', title: 'Italic' }, + { + label: 'S', + syntax: '~~', + placeholder: 'strikethrough', + title: 'Strikethrough', + }, + { label: '`', syntax: '`', placeholder: 'code', title: 'Inline Code' }, + ] as const + ).map(({ label, syntax, placeholder, title }) => ( + + ))} + + )} +