@@ -34,8 +34,10 @@ export interface UrlPreviewGroupViewModelProps {
3434}
3535
3636export const MAX_PREVIEWS_WHEN_LIMITED = 2 ;
37- export const PREVIEW_WIDTH = 100 ;
38- export const PREVIEW_HEIGHT = 100 ;
37+ export const PREVIEW_WIDTH_PX = 478 ;
38+ export const PREVIEW_HEIGHT_PX = 200 ;
39+ export const MIN_PREVIEW_PX = 96 ;
40+ export const MIN_IMAGE_SIZE_BYTES = 8192 ;
3941
4042export enum PreviewVisibility {
4143 /**
@@ -100,28 +102,77 @@ export class UrlPreviewGroupViewModel
100102 typeof response [ "og:description" ] === "string" && response [ "og:description" ] . trim ( )
101103 ? response [ "og:description" ] . trim ( )
102104 : undefined ;
103- let siteName =
105+ const siteName =
104106 typeof response [ "og:site_name" ] === "string" && response [ "og:site_name" ] . trim ( )
105107 ? response [ "og:site_name" ] . trim ( )
106- : undefined ;
108+ : new URL ( link ) . hostname ;
107109
110+ // If there is no title, use the description as the title.
108111 if ( ! title && description ) {
109112 title = description ;
110113 description = undefined ;
111114 } else if ( ! title && siteName ) {
112115 title = siteName ;
113- siteName = undefined ;
114116 } else if ( ! title ) {
115117 title = link ;
116118 }
117119
120+ // If the description matches the site name, don't bother with a description.
121+ if ( description && description . toLowerCase ( ) === siteName . toLowerCase ( ) ) {
122+ description = undefined ;
123+ }
124+
118125 return {
119126 title,
120127 description : description && decode ( description ) ,
121128 siteName,
122129 } ;
123130 }
124131
132+ /**
133+ * Calculate the best possible author from an opengraph response.
134+ * @param response The opengraph response
135+ * @returns The author value, or undefined if no valid author could be found.
136+ */
137+ private static getAuthorFromResponse ( response : IPreviewUrlResponse ) : UrlPreview [ "author" ] {
138+ let calculatedAuthor : string | undefined ;
139+ if ( response [ "og:type" ] === "article" ) {
140+ if ( typeof response [ "article:author" ] === "string" && response [ "article:author" ] ) {
141+ calculatedAuthor = response [ "article:author" ] ;
142+ }
143+ // Otherwise fall through to check the profile.
144+ }
145+ if ( typeof response [ "profile:username" ] === "string" && response [ "profile:username" ] ) {
146+ calculatedAuthor = response [ "profile:username" ] ;
147+ }
148+ if ( calculatedAuthor && URL . canParse ( calculatedAuthor ) ) {
149+ // Some sites return URLs as authors which doesn't look good in Element, so discard it.
150+ return ;
151+ }
152+ return calculatedAuthor ;
153+ }
154+
155+ /**
156+ * Calculate whether the provided image from the preview response is an full size preview or
157+ * a site icon.
158+ * @returns `true` if the image should be used as a preview, otherwise `false`
159+ */
160+ private static isImagePreview ( width ?: number , height ?: number , bytes ?: number ) : boolean {
161+ // We can't currently distinguish from a preview image and a favicon. Neither OpenGraph nor Matrix
162+ // have a clear distinction, so we're using a heuristic here to check the dimensions & size of the file and
163+ // deciding whether to render it as a full preview or icon.
164+ if ( width && width < MIN_PREVIEW_PX ) {
165+ return false ;
166+ }
167+ if ( height && height < MIN_PREVIEW_PX ) {
168+ return false ;
169+ }
170+ if ( bytes && bytes < MIN_IMAGE_SIZE_BYTES ) {
171+ return false ;
172+ }
173+ return true ;
174+ }
175+
125176 /**
126177 * Determine if an anchor element can be rendered into a preview.
127178 * If it can, return the value of `href`
@@ -278,38 +329,54 @@ export class UrlPreviewGroupViewModel
278329 }
279330
280331 const { title, description, siteName } = UrlPreviewGroupViewModel . getBaseMetadataFromResponse ( preview , link ) ;
332+ const author = UrlPreviewGroupViewModel . getAuthorFromResponse ( preview ) ;
281333 const hasImage = preview [ "og:image" ] && typeof preview ?. [ "og:image" ] === "string" ;
282334 // Ensure we have something relevant to render.
283335 // The title must not just be the link, or we must have an image.
284336 if ( title === link && ! hasImage ) {
285337 return null ;
286338 }
287339 let image : UrlPreview [ "image" ] ;
340+ let siteIcon : string | undefined ;
288341 if ( typeof preview [ "og:image" ] === "string" && this . visibility > PreviewVisibility . MediaHidden ) {
289342 const media = mediaFromMxc ( preview [ "og:image" ] , this . client ) ;
290343 const declaredHeight = UrlPreviewGroupViewModel . getNumberFromOpenGraph ( preview [ "og:image:height" ] ) ;
291344 const declaredWidth = UrlPreviewGroupViewModel . getNumberFromOpenGraph ( preview [ "og:image:width" ] ) ;
292- const width = Math . min ( declaredWidth ?? PREVIEW_WIDTH , PREVIEW_WIDTH ) ;
293- const height = thumbHeight ( width , declaredHeight , PREVIEW_WIDTH , PREVIEW_WIDTH ) ?? PREVIEW_WIDTH ;
294- const thumb = media . getThumbnailOfSourceHttp ( PREVIEW_WIDTH , PREVIEW_HEIGHT , "scale" ) ;
295- // No thumb, no preview.
296- if ( thumb ) {
297- image = {
298- imageThumb : thumb ,
299- imageFull : media . srcHttp ?? thumb ,
300- width,
301- height,
302- fileSize : UrlPreviewGroupViewModel . getNumberFromOpenGraph ( preview [ "matrix:image:size" ] ) ,
303- } ;
345+ const imageSize = UrlPreviewGroupViewModel . getNumberFromOpenGraph ( preview [ "matrix:image:size" ] ) ;
346+ const alt = typeof preview [ "og:image:alt" ] === "string" ? preview [ "og:image:alt" ] : undefined ;
347+
348+ const isImagePreview = UrlPreviewGroupViewModel . isImagePreview ( declaredWidth , declaredHeight , imageSize ) ;
349+ if ( isImagePreview ) {
350+ const width = Math . min ( declaredWidth ?? PREVIEW_WIDTH_PX , PREVIEW_WIDTH_PX ) ;
351+ const height =
352+ thumbHeight ( width , declaredHeight , PREVIEW_WIDTH_PX , PREVIEW_WIDTH_PX ) ?? PREVIEW_WIDTH_PX ;
353+ const thumb = media . getThumbnailOfSourceHttp ( PREVIEW_WIDTH_PX , PREVIEW_HEIGHT_PX , "scale" ) ;
354+ const playable = ! ! preview [ "og:video" ] || ! ! preview [ "og:video:type" ] || ! ! preview [ "og:audio" ] ;
355+ // No thumb, no preview.
356+ if ( thumb ) {
357+ image = {
358+ imageThumb : thumb ,
359+ imageFull : media . srcHttp ?? thumb ,
360+ width,
361+ height,
362+ fileSize : UrlPreviewGroupViewModel . getNumberFromOpenGraph ( preview [ "matrix:image:size" ] ) ,
363+ alt,
364+ playable,
365+ } ;
366+ }
367+ } else if ( media . srcHttp ) {
368+ siteIcon = media . srcHttp ;
304369 }
305370 }
306371
307372 const result = {
308373 link,
309374 title,
375+ author,
310376 description,
311377 siteName,
312- showTooltipOnLink : link !== title && PlatformPeg . get ( ) ?. needsUrlTooltips ( ) ,
378+ siteIcon,
379+ showTooltipOnLink : ! ! ( link !== title && PlatformPeg . get ( ) ?. needsUrlTooltips ( ) ) ,
313380 image,
314381 } satisfies UrlPreview ;
315382 this . previewCache . set ( link , result ) ;
0 commit comments