From f583e7eeb59ed3d1c65f0efe0da7a16877e93503 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 19:03:03 -0500 Subject: [PATCH 1/7] Card --> PreviewCard --- .../components/card.tsx => components/preview-card.tsx} | 6 +++--- src/components/status-media.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/{features/status/components/card.tsx => components/preview-card.tsx} (98%) diff --git a/src/features/status/components/card.tsx b/src/components/preview-card.tsx similarity index 98% rename from src/features/status/components/card.tsx rename to src/components/preview-card.tsx index fd205b723..d11479e03 100644 --- a/src/features/status/components/card.tsx +++ b/src/components/preview-card.tsx @@ -20,7 +20,7 @@ const trim = (text: string, len: number): string => { return text.substring(0, cut) + (text.length > len ? '…' : ''); }; -interface ICard { +interface IPreviewCard { card: CardEntity; maxTitle?: number; maxDescription?: number; @@ -31,7 +31,7 @@ interface ICard { horizontal?: boolean; } -const Card: React.FC = ({ +const PreviewCard: React.FC = ({ card, defaultWidth = 467, maxTitle = 120, @@ -253,4 +253,4 @@ const Card: React.FC = ({ ); }; -export default Card; +export default PreviewCard; diff --git a/src/components/status-media.tsx b/src/components/status-media.tsx index 7147e3a54..2c96c0078 100644 --- a/src/components/status-media.tsx +++ b/src/components/status-media.tsx @@ -2,9 +2,9 @@ import React, { Suspense } from 'react'; import { openModal } from 'soapbox/actions/modals'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import PreviewCard from 'soapbox/components/preview-card'; import { GroupLinkPreview } from 'soapbox/features/groups/components/group-link-preview'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card'; -import Card from 'soapbox/features/status/components/card'; import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch } from 'soapbox/hooks'; @@ -118,7 +118,7 @@ const StatusMedia: React.FC = ({ ); } else if (status.spoiler_text.length === 0 && !status.quote && status.card) { media = ( - Date: Tue, 10 Oct 2023 19:58:57 -0500 Subject: [PATCH 2/7] LinkPreview: check RTL of text --- src/components/preview-card.tsx | 40 ++++++++++++++---------- src/rtl.ts | 55 +++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/components/preview-card.tsx b/src/components/preview-card.tsx index d11479e03..756e7d528 100644 --- a/src/components/preview-card.tsx +++ b/src/components/preview-card.tsx @@ -3,23 +3,14 @@ import { List as ImmutableList } from 'immutable'; import React, { useState, useEffect } from 'react'; import Blurhash from 'soapbox/components/blurhash'; -import Icon from 'soapbox/components/icon'; -import { HStack, Stack, Text } from 'soapbox/components/ui'; +import { HStack, Stack, Text, Icon } from 'soapbox/components/ui'; import { normalizeAttachment } from 'soapbox/normalizers'; +import { getTextDirection } from 'soapbox/rtl'; import { addAutoPlay } from 'soapbox/utils/media'; import type { Card as CardEntity, Attachment } from 'soapbox/types/entities'; -const trim = (text: string, len: number): string => { - const cut = text.indexOf(' ', len); - - if (cut === -1) { - return text; - } - - return text.substring(0, cut) + (text.length > len ? '…' : ''); -}; - +/** Props for `PreviewCard`. */ interface IPreviewCard { card: CardEntity; maxTitle?: number; @@ -31,6 +22,7 @@ interface IPreviewCard { horizontal?: boolean; } +/** Displays a Mastodon link preview. Similar to OEmbed. */ const PreviewCard: React.FC = ({ card, defaultWidth = 467, @@ -48,6 +40,8 @@ const PreviewCard: React.FC = ({ setEmbedded(false); }, [card.url]); + const direction = getTextDirection(card.title + card.description); + const trimmedTitle = trim(card.title, maxTitle); const trimmedDescription = trim(card.description, maxDescription); @@ -123,26 +117,27 @@ const PreviewCard: React.FC = ({ title={trimmedTitle} rel='noopener' target='_blank' + dir={direction} > - {trimmedTitle} + {trimmedTitle} ) : ( - {trimmedTitle} + {trimmedTitle} ); const description = ( {trimmedTitle && ( - {title} + {title} )} {trimmedDescription && ( - {trimmedDescription} + {trimmedDescription} )} - + {card.provider_name} @@ -253,4 +248,15 @@ const PreviewCard: React.FC = ({ ); }; +/** Trim the text, adding ellipses if it's too long. */ +function trim(text: string, len: number): string { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.substring(0, cut) + (text.length > len ? '…' : ''); +} + export default PreviewCard; diff --git a/src/rtl.ts b/src/rtl.ts index d14518acc..3b9f1424f 100644 --- a/src/rtl.ts +++ b/src/rtl.ts @@ -1,38 +1,59 @@ -// U+0590 to U+05FF - Hebrew -// U+0600 to U+06FF - Arabic -// U+0700 to U+074F - Syriac -// U+0750 to U+077F - Arabic Supplement -// U+0780 to U+07BF - Thaana -// U+07C0 to U+07FF - N'Ko -// U+0800 to U+083F - Samaritan -// U+08A0 to U+08FF - Arabic Extended-A -// U+FB1D to U+FB4F - Hebrew presentation forms -// U+FB50 to U+FDFF - Arabic presentation forms A -// U+FE70 to U+FEFF - Arabic presentation forms B - +/** Unicode character ranges for RTL characters. */ const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; -/** Check if text is right-to-left (eg Arabic). */ -export function isRtl(text: string): boolean { +/** + * Check if text is right-to-left (eg Arabic). + * + * - U+0590 to U+05FF - Hebrew + * - U+0600 to U+06FF - Arabic + * - U+0700 to U+074F - Syriac + * - U+0750 to U+077F - Arabic Supplement + * - U+0780 to U+07BF - Thaana + * - U+07C0 to U+07FF - N'Ko + * - U+0800 to U+083F - Samaritan + * - U+08A0 to U+08FF - Arabic Extended-A + * - U+FB1D to U+FB4F - Hebrew presentation forms + * - U+FB50 to U+FDFF - Arabic presentation forms A + * - U+FE70 to U+FEFF - Arabic presentation forms B + */ +function isRtl(text: string, confidence = 0.3): boolean { if (text.length === 0) { return false; } + // Remove http(s), (s)ftp, ws(s), blob and smtp(s) links text = text.replace(/(?:https?|ftp|sftp|ws|wss|blob|smtp|smtps):\/\/[\S]+/g, ''); // Remove email address links text = text.replace(/(mailto:)([^\s@]+@[^\s@]+\.[^\s@]+)/g, ''); - // Remove Phone numbe links + // Remove phone number links text = text.replace(/(tel:)([+\d\s()-]+)/g, ''); + // Remove mentions text = text.replace(/(?:^|[^/\w])@([a-z0-9_]+(@[a-z0-9.-]+)?)/ig, ''); + // Remove hashtags text = text.replace(/(?:^|[^/\w])#([\S]+)/ig, ''); + // Remove all non-word characters text = text.replace(/\s+/g, ''); const matches = text.match(rtlChars); if (!matches) { - return false; } - return matches.length / text.length > 0.3; + return matches.length / text.length > confidence; +} + +interface GetTextDirectionOpts { + /** The default direction to return if the text is empty. */ + fallback?: 'ltr' | 'rtl'; + /** The confidence threshold (0-1) to use when determining the direction. */ + confidence?: number; +} + +/** Get the direction of the text. */ +function getTextDirection(text: string, { fallback = 'ltr', confidence }: GetTextDirectionOpts = {}): 'ltr' | 'rtl' { + if (!text) return fallback; + return isRtl(text, confidence) ? 'rtl' : 'ltr'; } + +export { getTextDirection, isRtl }; \ No newline at end of file From ae63d37d7876dd3e62bb9fc980fb0abd5826e918 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 20:02:22 -0500 Subject: [PATCH 3/7] rtl.ts -> utils/rtl.ts --- src/components/autosuggest-input.tsx | 2 +- src/components/autosuggest-textarea.tsx | 2 +- src/components/preview-card.tsx | 2 +- src/components/status-content.tsx | 4 ++-- src/features/compose/components/reply-indicator.tsx | 4 ++-- src/{ => utils}/rtl.ts | 0 6 files changed, 7 insertions(+), 7 deletions(-) rename src/{ => utils}/rtl.ts (100%) diff --git a/src/components/autosuggest-input.tsx b/src/components/autosuggest-input.tsx index 1846b02d0..b41934e7c 100644 --- a/src/components/autosuggest-input.tsx +++ b/src/components/autosuggest-input.tsx @@ -7,7 +7,7 @@ import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji'; import Icon from 'soapbox/components/icon'; import { Input, Portal } from 'soapbox/components/ui'; import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account'; -import { isRtl } from 'soapbox/rtl'; +import { isRtl } from 'soapbox/utils/rtl'; import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu'; diff --git a/src/components/autosuggest-textarea.tsx b/src/components/autosuggest-textarea.tsx index bc19c15e5..233c1e4e3 100644 --- a/src/components/autosuggest-textarea.tsx +++ b/src/components/autosuggest-textarea.tsx @@ -5,7 +5,7 @@ import Textarea from 'react-textarea-autosize'; import { Portal } from 'soapbox/components/ui'; import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account'; -import { isRtl } from 'soapbox/rtl'; +import { isRtl } from 'soapbox/utils/rtl'; import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions'; import AutosuggestEmoji from './autosuggest-emoji'; diff --git a/src/components/preview-card.tsx b/src/components/preview-card.tsx index 756e7d528..16a1a467c 100644 --- a/src/components/preview-card.tsx +++ b/src/components/preview-card.tsx @@ -5,8 +5,8 @@ import React, { useState, useEffect } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import { HStack, Stack, Text, Icon } from 'soapbox/components/ui'; import { normalizeAttachment } from 'soapbox/normalizers'; -import { getTextDirection } from 'soapbox/rtl'; import { addAutoPlay } from 'soapbox/utils/media'; +import { getTextDirection } from 'soapbox/utils/rtl'; import type { Card as CardEntity, Attachment } from 'soapbox/types/entities'; diff --git a/src/components/status-content.tsx b/src/components/status-content.tsx index 7c322dcb8..ffc044df2 100644 --- a/src/components/status-content.tsx +++ b/src/components/status-content.tsx @@ -6,7 +6,7 @@ import { useHistory } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { onlyEmoji as isOnlyEmoji } from 'soapbox/utils/rich-content'; -import { isRtl } from '../rtl'; +import { getTextDirection } from '../utils/rtl'; import Markup from './markup'; import Poll from './polls/poll'; @@ -142,7 +142,7 @@ const StatusContent: React.FC = ({ const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none'; const content = { __html: parsedHtml }; - const direction = isRtl(status.search_index) ? 'rtl' : 'ltr'; + const direction = getTextDirection(status.search_index); const className = clsx(baseClassName, { 'cursor-pointer': onClick, 'whitespace-normal': withSpoiler, diff --git a/src/features/compose/components/reply-indicator.tsx b/src/features/compose/components/reply-indicator.tsx index d5478d204..bee36b661 100644 --- a/src/features/compose/components/reply-indicator.tsx +++ b/src/features/compose/components/reply-indicator.tsx @@ -5,7 +5,7 @@ import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; import Markup from 'soapbox/components/markup'; import { Stack } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; -import { isRtl } from 'soapbox/rtl'; +import { getTextDirection } from 'soapbox/utils/rtl'; import type { Status } from 'soapbox/types/entities'; @@ -50,7 +50,7 @@ const ReplyIndicator: React.FC = ({ className, status, hideActi className='break-words' size='sm' dangerouslySetInnerHTML={{ __html: status.contentHtml }} - direction={isRtl(status.search_index) ? 'rtl' : 'ltr'} + direction={getTextDirection(status.search_index)} /> {status.media_attachments.size > 0 && ( diff --git a/src/rtl.ts b/src/utils/rtl.ts similarity index 100% rename from src/rtl.ts rename to src/utils/rtl.ts From e0f43986a7b1b43f583dec19cea6f8b9f30ee6e6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 20:18:59 -0500 Subject: [PATCH 4/7] Chats: flip arrow icons in RTL --- .../chats/components/chat-page/components/chat-page-main.tsx | 2 +- .../chats/components/chat-page/components/chat-page-new.tsx | 2 +- .../components/chat-page/components/chat-page-settings.tsx | 2 +- src/features/chats/components/chat-widget/chat-settings.tsx | 2 +- src/features/chats/components/chat-widget/chat-window.tsx | 2 +- .../chats/components/chat-widget/headers/chat-search-header.tsx | 2 +- src/features/groups/discover.tsx | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/features/chats/components/chat-page/components/chat-page-main.tsx b/src/features/chats/components/chat-page/components/chat-page-main.tsx index 890983f64..89371c4da 100644 --- a/src/features/chats/components/chat-page/components/chat-page-main.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-main.tsx @@ -121,7 +121,7 @@ const ChatPageMain = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-page/components/chat-page-new.tsx b/src/features/chats/components/chat-page/components/chat-page-new.tsx index 0b1e0de8d..0a60c56b4 100644 --- a/src/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-new.tsx @@ -24,7 +24,7 @@ const ChatPageNew: React.FC = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-page/components/chat-page-settings.tsx b/src/features/chats/components/chat-page/components/chat-page-settings.tsx index c33f74013..404c56afd 100644 --- a/src/features/chats/components/chat-page/components/chat-page-settings.tsx +++ b/src/features/chats/components/chat-page/components/chat-page-settings.tsx @@ -51,7 +51,7 @@ const ChatPageSettings = () => { history.push('/chats')} /> diff --git a/src/features/chats/components/chat-widget/chat-settings.tsx b/src/features/chats/components/chat-widget/chat-settings.tsx index 5a38778e0..0e62f3cfd 100644 --- a/src/features/chats/components/chat-widget/chat-settings.tsx +++ b/src/features/chats/components/chat-widget/chat-settings.tsx @@ -96,7 +96,7 @@ const ChatSettings = () => { diff --git a/src/features/chats/components/chat-widget/chat-window.tsx b/src/features/chats/components/chat-widget/chat-window.tsx index 84845d7f7..66c02397f 100644 --- a/src/features/chats/components/chat-widget/chat-window.tsx +++ b/src/features/chats/components/chat-widget/chat-window.tsx @@ -73,7 +73,7 @@ const ChatWindow = () => { )} diff --git a/src/features/chats/components/chat-widget/headers/chat-search-header.tsx b/src/features/chats/components/chat-widget/headers/chat-search-header.tsx index d0fd40921..15b0c6a4b 100644 --- a/src/features/chats/components/chat-widget/headers/chat-search-header.tsx +++ b/src/features/chats/components/chat-widget/headers/chat-search-header.tsx @@ -27,7 +27,7 @@ const ChatSearchHeader = () => { > diff --git a/src/features/groups/discover.tsx b/src/features/groups/discover.tsx index 47273d2ed..f91c3ca48 100644 --- a/src/features/groups/discover.tsx +++ b/src/features/groups/discover.tsx @@ -37,7 +37,7 @@ const Discover: React.FC = () => { {isSearching ? ( From 10f4ea307989a3b2abbb92f81ca60cd52b85abff Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 20:28:00 -0500 Subject: [PATCH 5/7] ErrorBoundary: fix RTL arrow --- src/components/error-boundary.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index 8c45cea1e..a80ef43dc 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -152,7 +152,8 @@ class ErrorBoundary extends React.PureComponent { From fd592460a4765c9e47b0e82f1de26805e8750d09 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 20:29:31 -0500 Subject: [PATCH 6/7] ErrorBoundary: we do a little refactoring --- src/components/error-boundary.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index a80ef43dc..494a813b6 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -14,18 +14,6 @@ import SiteLogo from './site-logo'; import type { RootState } from 'soapbox/store'; -const goHome = () => location.href = '/'; - -const mapStateToProps = (state: RootState) => { - const { links, logo } = getSoapboxConfig(state); - - return { - siteTitle: state.instance.title, - logo, - links, - }; -}; - interface Props extends ReturnType { children: React.ReactNode; } @@ -216,4 +204,18 @@ class ErrorBoundary extends React.PureComponent { } +function goHome() { + location.href = '/'; +} + +function mapStateToProps(state: RootState) { + const { links, logo } = getSoapboxConfig(state); + + return { + siteTitle: state.instance.title, + logo, + links, + }; +} + export default connect(mapStateToProps)(ErrorBoundary); From cad377c910c9073e61f89cd9cfa29777211160db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 10 Oct 2023 20:31:45 -0500 Subject: [PATCH 7/7] ErrorBoundary: ensure error text is LTR --- src/components/error-boundary.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx index 494a813b6..b25ca2aae 100644 --- a/src/components/error-boundary.tsx +++ b/src/components/error-boundary.tsx @@ -154,6 +154,7 @@ class ErrorBoundary extends React.PureComponent { className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm' value={errorText} onClick={this.handleCopy} + dir='ltr' readOnly /> )}