mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Shorten links in composer to reduce char usage (#1188)

* Modify toShortUrl() to always include the full domain

* Shorten links in the composer to save on characters

* Apply some limits to the link card suggester

authored by

Paul Frazee and committed by
GitHub
819340dd 53795619

+123 -26
+57 -1
__tests__/lib/string.test.ts
··· 1 + import {RichText} from '@atproto/api' 1 2 import { 2 3 getYoutubeVideoId, 3 4 makeRecordUri, ··· 8 9 import {pluralize, enforceLen} from '../../src/lib/strings/helpers' 9 10 import {ago} from '../../src/lib/strings/time' 10 11 import {detectLinkables} from '../../src/lib/strings/rich-text-detection' 12 + import {shortenLinks} from '../../src/lib/strings/rich-text-manip' 11 13 import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' 12 14 import {cleanError} from '../../src/lib/strings/errors' 13 15 ··· 296 298 'https://bsky.app', 297 299 'https://bsky.app/3jk7x4irgv52r', 298 300 'https://bsky.app/3jk7x4irgv52r2313y182h9', 301 + 'https://very-long-domain-name.com/foo', 302 + 'https://very-long-domain-name.com/foo?bar=baz#andsomemore', 299 303 ] 300 304 const outputs = [ 301 305 'bsky.app', 302 306 'bsky.app/3jk7x4irgv52r', 303 - 'bsky.app/3jk7x4irgv52r2313y...', 307 + 'bsky.app/3jk7x4irgv52...', 308 + 'very-long-domain-name.com/foo', 309 + 'very-long-domain-name.com/foo?bar=baz#...', 304 310 ] 305 311 306 312 it('shortens the url', () => { ··· 352 358 expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId') 353 359 }) 354 360 }) 361 + 362 + describe('shortenLinks', () => { 363 + const inputs = [ 364 + 'start https://middle.com/foo/bar?baz=bux#hash end', 365 + 'https://start.com/foo/bar?baz=bux#hash middle end', 366 + 'start middle https://end.com/foo/bar?baz=bux#hash', 367 + 'https://newline1.com/very/long/url/here\nhttps://newline2.com/very/long/url/here', 368 + 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/', 369 + ] 370 + const outputs = [ 371 + [ 372 + 'start middle.com/foo/bar?baz=... end', 373 + ['https://middle.com/foo/bar?baz=bux#hash'], 374 + ], 375 + [ 376 + 'start.com/foo/bar?baz=... middle end', 377 + ['https://start.com/foo/bar?baz=bux#hash'], 378 + ], 379 + [ 380 + 'start middle end.com/foo/bar?baz=...', 381 + ['https://end.com/foo/bar?baz=bux#hash'], 382 + ], 383 + [ 384 + 'newline1.com/very/long/ur...\nnewline2.com/very/long/ur...', 385 + [ 386 + 'https://newline1.com/very/long/url/here', 387 + 'https://newline2.com/very/long/url/here', 388 + ], 389 + ], 390 + [ 391 + 'Classic article socket3.wordpress.com/2018/02/03/d...', 392 + [ 393 + 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/', 394 + ], 395 + ], 396 + ] 397 + it('correctly shortens rich text while preserving facet URIs', () => { 398 + for (let i = 0; i < inputs.length; i++) { 399 + const input = inputs[i] 400 + const inputRT = new RichText({text: input}) 401 + inputRT.detectFacetsWithoutResolution() 402 + const outputRT = shortenLinks(inputRT) 403 + expect(outputRT.text).toEqual(outputs[i][0]) 404 + expect(outputRT.facets?.length).toEqual(outputs[i][1].length) 405 + for (let j = 0; j < outputs[i][1].length; j++) { 406 + expect(outputRT.facets![j].features[0].uri).toEqual(outputs[i][1][j]) 407 + } 408 + } 409 + }) 410 + })
+3 -1
src/lib/api/index.ts
··· 14 14 import {LinkMeta} from '../link-meta/link-meta' 15 15 import {isWeb} from 'platform/detection' 16 16 import {ImageModel} from 'state/models/media/image' 17 + import {shortenLinks} from 'lib/strings/rich-text-manip' 17 18 18 19 export interface ExternalEmbedDraft { 19 20 uri: string ··· 92 93 | AppBskyEmbedRecordWithMedia.Main 93 94 | undefined 94 95 let reply 95 - const rt = new RichText( 96 + let rt = new RichText( 96 97 {text: opts.rawText.trim()}, 97 98 { 98 99 cleanNewlines: true, ··· 101 102 102 103 opts.onStateChange?.('Processing...') 103 104 await rt.detectFacets(store.agent) 105 + rt = shortenLinks(rt) 104 106 105 107 // filter out any mention facets that didn't map to a user 106 108 rt.facets = rt.facets?.filter(facet => {
+34
src/lib/strings/rich-text-manip.ts
··· 1 + import {RichText, UnicodeString} from '@atproto/api' 2 + import {toShortUrl} from './url-helpers' 3 + 4 + export function shortenLinks(rt: RichText): RichText { 5 + if (!rt.facets?.length) { 6 + return rt 7 + } 8 + rt = rt.clone() 9 + // enumerate the link facets 10 + if (rt.facets) { 11 + for (const facet of rt.facets) { 12 + const isLink = !!facet.features.find( 13 + f => f.$type === 'app.bsky.richtext.facet#link', 14 + ) 15 + if (!isLink) { 16 + continue 17 + } 18 + 19 + // extract and shorten the URL 20 + const {byteStart, byteEnd} = facet.index 21 + const url = rt.unicodeText.slice(byteStart, byteEnd) 22 + const shortened = new UnicodeString(toShortUrl(url)) 23 + 24 + // insert the shorten URL 25 + rt.insert(byteStart, shortened.utf16) 26 + // update the facet to cover the new shortened URL 27 + facet.index.byteStart = byteStart 28 + facet.index.byteEnd = byteStart + shortened.length 29 + // remove the old URL 30 + rt.delete(byteStart + shortened.length, byteEnd + shortened.length) 31 + } 32 + } 33 + return rt 34 + }
+5 -8
src/lib/strings/url-helpers.ts
··· 42 42 if (urlp.protocol !== 'http:' && urlp.protocol !== 'https:') { 43 43 return url 44 44 } 45 - const shortened = 46 - urlp.host + 47 - (urlp.pathname === '/' ? '' : urlp.pathname) + 48 - urlp.search + 49 - urlp.hash 50 - if (shortened.length > 30) { 51 - return shortened.slice(0, 27) + '...' 45 + const path = 46 + (urlp.pathname === '/' ? '' : urlp.pathname) + urlp.search + urlp.hash 47 + if (path.length > 15) { 48 + return urlp.host + path.slice(0, 13) + '...' 52 49 } 53 - return shortened ? shortened : url 50 + return urlp.host + path 54 51 } catch (e) { 55 52 return url 56 53 }
+23 -16
src/view/com/composer/Composer.tsx
··· 32 32 import {sanitizeDisplayName} from 'lib/strings/display-names' 33 33 import {sanitizeHandle} from 'lib/strings/handles' 34 34 import {cleanError} from 'lib/strings/errors' 35 + import {shortenLinks} from 'lib/strings/rich-text-manip' 36 + import {toShortUrl} from 'lib/strings/url-helpers' 35 37 import {SelectPhotoBtn} from './photos/SelectPhotoBtn' 36 38 import {OpenCameraBtn} from './photos/OpenCameraBtn' 37 39 import {usePalette} from 'lib/hooks/usePalette' ··· 63 65 const [processingState, setProcessingState] = useState('') 64 66 const [error, setError] = useState('') 65 67 const [richtext, setRichText] = useState(new RichText({text: ''})) 66 - const graphemeLength = useMemo(() => richtext.graphemeLength, [richtext]) 68 + const graphemeLength = useMemo(() => { 69 + return shortenLinks(richtext).graphemeLength 70 + }, [richtext]) 67 71 const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>( 68 72 initQuote, 69 73 ) ··· 148 152 ) 149 153 150 154 const onPressPublish = async (rt: RichText) => { 151 - if (isProcessing || rt.graphemeLength > MAX_GRAPHEME_LENGTH) { 155 + if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { 152 156 return 153 157 } 154 158 if (store.preferences.requireAltTextEnabled && gallery.needsAltText) { ··· 352 356 </ScrollView> 353 357 {!extLink && suggestedLinks.size > 0 ? ( 354 358 <View style={s.mb5}> 355 - {Array.from(suggestedLinks).map(url => ( 356 - <TouchableOpacity 357 - key={`suggested-${url}`} 358 - testID="addLinkCardBtn" 359 - style={[pal.borderDark, styles.addExtLinkBtn]} 360 - onPress={() => onPressAddLinkCard(url)} 361 - accessibilityRole="button" 362 - accessibilityLabel="Add link card" 363 - accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> 364 - <Text style={pal.text}> 365 - Add link card: <Text style={pal.link}>{url}</Text> 366 - </Text> 367 - </TouchableOpacity> 368 - ))} 359 + {Array.from(suggestedLinks) 360 + .slice(0, 3) 361 + .map(url => ( 362 + <TouchableOpacity 363 + key={`suggested-${url}`} 364 + testID="addLinkCardBtn" 365 + style={[pal.borderDark, styles.addExtLinkBtn]} 366 + onPress={() => onPressAddLinkCard(url)} 367 + accessibilityRole="button" 368 + accessibilityLabel="Add link card" 369 + accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> 370 + <Text style={pal.text}> 371 + Add link card:{' '} 372 + <Text style={pal.link}>{toShortUrl(url)}</Text> 373 + </Text> 374 + </TouchableOpacity> 375 + ))} 369 376 </View> 370 377 ) : null} 371 378 <View style={[pal.border, styles.bottomBar]}>
+1
src/view/com/composer/text-input/TextInput.web.tsx
··· 107 107 const json = editorProp.getJSON() 108 108 109 109 const newRt = new RichText({text: editorJsonToText(json).trim()}) 110 + newRt.detectFacetsWithoutResolution() 110 111 setRichText(newRt) 111 112 112 113 const newSuggestedLinks = new Set(editorJsonToLinks(json))