import marked from 'marked' ; import { IntlShape } from 'react-intl' ; import moment from 'moment' ; import { Block } from './blocks/block' ; import { createBoard } from './blocks/board' ; import { createBoardView } from './blocks/boardView' ; import { createCard } from './blocks/card' ; import { createCommentBlock } from './blocks/commentBlock' ; import { IAppWindow } from './types' ; declare let window ; const imageURLForUser = ( window as any ) . Components ?. imageURLForUser ; const IconClass = 'octo-icon' ; const OpenButtonClass = 'open-button' ; const SpacerClass = 'octo-spacer' ; const HorizontalGripClass = 'HorizontalGrip' ; const base32Alphabet = 'ybndrfg8ejkmcpqxot1uwisza345h769' ; enum IDType { None = '7' , Workspace = 'w' , Board = 'b' , Card = 'c' , View = 'v' , Session = 's' , User = 'u' , Token = 'k' , BlockID = 'a' } class Utils { static createGuid ( idType ) { const data = Utils . randomArray ( 16 ) ; return idType + this . base32encode ( data , false ) ; } static blockTypeToIDType ( blockType ) { let ret = IDType . None ; switch ( blockType ) { case 'workspace' : ret = IDType . Workspace ; break ; case 'board' : ret = IDType . Board ; break ; case 'card' : ret = IDType . Card ; break ; case 'view' : ret = IDType . View ; break ; } return ret ; } static getProfilePicture ( userId ? ) { const defaultImageUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>' ; return imageURLForUser && userId ? imageURLForUser ( userId ) : defaultImageUrl ; } static randomArray ( size ) { const crypto = window . crypto || window . msCrypto ; const rands = new Uint8Array ( size ) ; if ( crypto && crypto . getRandomValues ) { crypto . getRandomValues ( rands ) ; } else { for ( let i = 0 ; i < size ; i ++ ) { rands [ i ] = Math . floor ( ( Math . random ( ) * 255 ) ) ; } } return rands ; } static base32encode ( data , pad ) { const dview = new DataView ( data . buffer , data . byteOffset , data . byteLength ) ; let bits = 0 ; let value = 0 ; let output = '' ; for ( let i = 0 ; i < dview . byteLength ; i ++ ) { value = ( value << 8 ) | dview . getUint8 ( i ) ; bits += 8 ; while ( bits >= 5 ) { output += base32Alphabet [ ( value >>> ( bits - 5 ) ) & 31 ] ; bits -= 5 ; } } if ( bits > 0 ) { output += base32Alphabet [ ( value << ( 5 - bits ) ) & 31 ] ; } if ( pad ) { while ( ( output . length % 8 ) !== 0 ) { output += '=' ; } } return output ; } static htmlToElement ( html ) { const template = document . createElement ( 'template' ) ; template . innerHTML = html . trim ( ) ; return template . content . firstChild as HTMLElement ; } static getElementById ( elementId ) { const element = document . getElementById ( elementId ) ; Utils . assert ( element , `getElementById "${ elementId }$` ) ; return element ! ; } static htmlEncode ( text ) { return String ( text ) . replace ( /&/g , '&amp;' ) . replace ( /</g , '&lt;' ) . replace ( />/g , '&gt;' ) . replace ( /"/g , '&quot;' ) ; } static htmlDecode ( text ) { return String ( text ) . replace ( /&amp;/g , '&' ) . replace ( /&lt;/g , '<' ) . replace ( /&gt;/g , '>' ) . replace ( /&quot;/g , '"' ) ; } static canvas : HTMLCanvasElement | undefined ; static getTextWidth ( displayText , fontDescriptor ) { if ( displayText !== '' ) { if ( ! this . canvas ) { this . canvas = document . createElement ( 'canvas' ) as HTMLCanvasElement ; } const context = this . canvas . getContext ( '2d' ) ; if ( context ) { context . font = fontDescriptor ; const metrics = context . measureText ( displayText ) ; return Math . ceil ( metrics . width ) ; } } return 0 ; } static getFontAndPaddingFromCell = ( cell ) : { fontDescriptor : string ; padding : number ; } => { const style = getComputedStyle ( cell ) ; const padding = Utils . getTotalHorizontalPadding ( style ) ; return Utils . getFontAndPaddingFromChildren ( cell . children , padding ) ; } ; static getFontAndPaddingFromChildren = ( children , pad ) : { fontDescriptor : string ; padding : number ; } => { const myResults = { fontDescriptor : '' , padding : pad , } ; Array . from ( children ) . forEach ( ( element ) => { const style = getComputedStyle ( element ) ; if ( element . tagName === 'svg' ) { myResults . padding += element . clientWidth ; myResults . padding += Utils . getHorizontalBorder ( style ) ; myResults . padding += Utils . getHorizontalMargin ( style ) ; myResults . fontDescriptor = Utils . getFontString ( style ) ; } else { switch ( element . className ) { case IconClass : case HorizontalGripClass : myResults . padding += element . clientWidth ; break ; case SpacerClass : case OpenButtonClass : break ; default : { myResults . fontDescriptor = Utils . getFontString ( style ) ; myResults . padding += Utils . getTotalHorizontalPadding ( style ) ; const childResults = Utils . getFontAndPaddingFromChildren ( element . children , myResults . padding ) ; if ( childResults . fontDescriptor !== '' ) { myResults . fontDescriptor = childResults . fontDescriptor ; myResults . padding = childResults . padding ; } } } } } ) ; return myResults ; } ; private static getFontString ( style ) { if ( style . font ) { return style . font ; } const { fontStyle , fontVariant , fontWeight , fontSize , lineHeight , fontFamily } = style ; const props = [ fontStyle , fontVariant , fontWeight ] ; if ( fontSize ) { props . push ( lineHeight ? `${ fontSize } / ${ lineHeight }` : fontSize ) ; } props . push ( fontFamily ) ; return props . join ( ' ' ) ; } private static getHorizontalMargin ( style ) { return parseInt ( style . marginLeft , 10 ) + parseInt ( style . marginRight , 10 ) ; } private static getHorizontalBorder ( style ) { return parseInt ( style . borderLeftWidth , 10 ) + parseInt ( style . borderRightWidth , 10 ) ; } private static getHorizontalPadding ( style ) { return parseInt ( style . paddingLeft , 10 ) + parseInt ( style . paddingRight , 10 ) ; } private static getTotalHorizontalPadding ( style ) { return Utils . getHorizontalPadding ( style ) + Utils . getHorizontalMargin ( style ) + Utils . getHorizontalBorder ( style ) ; } static htmlFromMarkdown ( text ) { const renderer = new marked . Renderer ( ) ; renderer . link = ( href , title , contents ) => { return '<a ' + 'target="_blank" ' + 'rel="noreferrer" ' + `href="${ encodeURI ( href || '' ) }" ` + `title="${ title ? encodeURI ( title ) : '' }" ` + `onclick="${ ( window . openInNewBrowser ? ' openInNewBrowser && openInNewBrowser(event.target.href);' : '' ) }"` + '>' + contents + '</a>' ; } ; renderer . table = ( header , body ) => { return `<div class="table-responsive"><table class="markdown__table"><thead>${ header }</thead><tbody>${ body }</tbody></table></div>` ; } ; return this . htmlFromMarkdownWithRenderer ( text , renderer ) ; } static htmlFromMarkdownWithRenderer ( text , renderer ) { const html = marked ( text . replace ( /</g , '&lt;' ) , { renderer , breaks : true } ) ; return html . trim ( ) ; } static countCheckboxesInMarkdown ( text ) { let total = 0 ; let checked = 0 ; const renderer = new marked . Renderer ( ) ; renderer . checkbox = ( isChecked ) => { ++ total ; if ( isChecked ) { ++ checked ; } return '' ; } ; this . htmlFromMarkdownWithRenderer ( text , renderer ) ; return { total , checked } ; } private static yearOption ( date ) { const isCurrentYear = date . getFullYear ( ) === new Date ( ) . getFullYear ( ) ; return isCurrentYear ? undefined : 'numeric' ; } static displayDate ( date , intl ) { return intl . formatDate ( date , { year : Utils . yearOption ( date ) , month : 'long' , day : '2-digit' , } ) ; } static inputDate ( date , intl ) { return intl . formatDate ( date , { year : 'numeric' , month : '2-digit' , day : '2-digit' , } ) ; } static displayDateTime ( date , intl ) { return intl . formatDate ( date , { year : Utils . yearOption ( date ) , month : 'long' , day : '2-digit' , hour : 'numeric' , minute : 'numeric' , } ) ; } static relativeDisplayDateTime ( date , intl ) { return moment ( date ) . locale ( intl . locale . toLowerCase ( ) ) . fromNow ( ) ; } static sleep ( miliseconds ) { return new Promise ( ( resolve ) => setTimeout ( resolve , miliseconds ) ) ; } static assertValue ( valueObject ) { const name = Object . keys ( valueObject ) [ 0 ] ; const value = valueObject [ name ] ; if ( ! value ) { Utils . logError ( `ASSERT VALUE [${ name }]` ) ; } } static assert ( condition , tag = '' ) { if ( ! condition ) { Utils . logError ( `ASSERT [${ tag ?? new Error ( ) . stack }]` ) ; } } static assertFailure ( tag = '' ) { Utils . assert ( false , tag ) ; } static log ( message ) { const timestamp = ( Date . now ( ) / 1000 ) . toFixed ( 2 ) ; console . log ( `[${ timestamp }] ${ message }` ) ; } static logError ( message ) { const timestamp = ( Date . now ( ) / 1000 ) . toFixed ( 2 ) ; console . error ( `[${ timestamp }] ${ message }` ) ; } static logWarn ( message ) { const timestamp = ( Date . now ( ) / 1000 ) . toFixed ( 2 ) ; console . warn ( `[${ timestamp }] ${ message }` ) ; } static setFavicon ( icon ? ) { if ( Utils . isFocalboardPlugin ( ) ) { return ; } if ( ! icon ) { document . querySelector ( "link[rel*='icon']" ) ?. remove ( ) ; return ; } const link = document . createElement ( 'link' ) as HTMLLinkElement ; link . type = 'image/x-icon' ; link . rel = 'shortcut icon' ; link . href = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">${ icon }</text></svg>` ; document . querySelectorAll ( "link[rel*='icon']" ) . forEach ( ( n ) => n . remove ( ) ) ; document . getElementsByTagName ( 'head' ) [ 0 ] . appendChild ( link ) ; } static replaceUrlQueryParam ( paramName , value ? ) { const queryString = new URLSearchParams ( window . location . search ) ; const currentValue = queryString . get ( paramName ) || '' ; if ( currentValue !== value ) { const newUrl = new URL ( window . location . toString ( ) ) ; if ( value ) { newUrl . searchParams . set ( paramName , value ) ; } else { newUrl . searchParams . delete ( paramName ) ; } window . history . pushState ( { } , document . title , newUrl . toString ( ) ) ; } } static ensureProtocol ( url ) { return url . match ( /^.+:\/\// ) ? url : `https://${ url }` ; } static sanitizeFilename ( filename ) { let sanitizedFilename = filename ; const illegalCharacters = [ '\\' , '/' , '?' , ':' , '<' , '>' , '*' , '|' , '"' , '.' ] ; illegalCharacters . forEach ( ( character ) => { sanitizedFilename = sanitizedFilename . replace ( character , '' ) ; } ) ; return sanitizedFilename ; } static selectLocalFile ( onSelect ? , accept = '.jpg,.jpeg,.png' ) { const input = document . createElement ( 'input' ) ; input . type = 'file' ; input . accept = accept ; input . onchange = async ( ) => { const file = input . files ! [ 0 ] ; onSelect ?. ( file ) ; } ; input . style . display = 'none' ; document . body . appendChild ( input ) ; input . click ( ) ; } static arraysEqual ( a , b ) { if ( a === b ) { return true ; } if ( a === null || b === null ) { return false ; } if ( a === undefined || b === undefined ) { return false ; } if ( a . length !== b . length ) { return false ; } for ( let i = 0 ; i < a . length ; ++ i ) { if ( a [ i ] !== b [ i ] ) { return false ; } } return true ; } static arrayMove ( arr , srcIndex , destIndex ) { arr . splice ( destIndex , 0 , arr . splice ( srcIndex , 1 ) [ 0 ] ) ; } static copyTextToClipboard ( text ) { const textField = document . createElement ( 'textarea' ) ; textField . innerText = text ; textField . style . position = 'fixed' ; textField . style . opacity = '0' ; document . body . appendChild ( textField ) ; textField . select ( ) ; let result = false ; try { result = document . execCommand ( 'copy' ) ; } catch ( err ) { Utils . logError ( `copyTextToClipboard ERROR: ${ err }` ) ; result = false ; } textField . remove ( ) ; return result ; } static isMobile ( ) { const toMatch = [ /Android/i , /webOS/i , /iPhone/i , /iPad/i , /iPod/i , /BlackBerry/i , /Windows Phone/i , ] ; return toMatch . some ( ( toMatchItem ) => { return navigator . userAgent . match ( toMatchItem ) ; } ) ; } static getBaseURL ( absolute ? ) { let baseURL = window . baseURL || '' ; baseURL = baseURL . replace ( /\/+$/ , '' ) ; if ( baseURL . indexOf ( '/' ) === 0 ) { baseURL = baseURL . slice ( 1 ) ; } if ( absolute ) { return window . location . origin + '/' + baseURL ; } return baseURL ; } static getFrontendBaseURL ( absolute ? ) { let frontendBaseURL = window . frontendBaseURL || this . getBaseURL ( absolute ) ; frontendBaseURL = frontendBaseURL . replace ( /\/+$/ , '' ) ; if ( frontendBaseURL . indexOf ( '/' ) === 0 ) { frontendBaseURL = frontendBaseURL . slice ( 1 ) ; } if ( absolute ) { return window . location . origin + '/' + frontendBaseURL ; } return frontendBaseURL ; } static buildURL ( path , absolute ? ) { const baseURL = this . getBaseURL ( ) ; let finalPath = baseURL + path ; if ( path . indexOf ( '/' ) !== 0 ) { finalPath = baseURL + '/' + path ; } if ( absolute ) { if ( finalPath . indexOf ( '/' ) === 0 ) { finalPath = finalPath . slice ( 1 ) ; } return window . location . origin + '/' + finalPath ; } return finalPath ; } static roundTo ( num , decimalPlaces ) { return Math . round ( num * Math . pow ( 10 , decimalPlaces ) ) / Math . pow ( 10 , decimalPlaces ) ; } static isFocalboardPlugin ( ) { return Boolean ( window . isFocalboardPlugin ) ; } static isFocalboardLegacy ( ) { return window . location . pathname . includes ( '/plugins/focalboard' ) ; } static fixBlock ( block ) { switch ( block . type ) { case 'board' : return createBoard ( block ) ; case 'view' : return createBoardView ( block ) ; case 'card' : return createCard ( block ) ; case 'comment' : return createCommentBlock ( block ) ; default : return block ; } } static userAgent ( ) { return window . navigator . userAgent ; } static isDesktopApp ( ) { return Utils . userAgent ( ) . indexOf ( 'Mattermost' ) !== - 1 && Utils . userAgent ( ) . indexOf ( 'Electron' ) !== - 1 ; } static getDesktopVersion ( ) { const regex = /Mattermost\/(\d+\.\d+\.\d+)/gm ; const match = regex . exec ( window . navigator . appVersion ) ?. [ 1 ] || '' ; return match ; } /**
     * Function to check how a version compares to another
     *
     * eg.  versionA = 4.16.0, versionB = 4.17.0 returns  1
     *      versionA = 4.16.1, versionB = 4.16.1 returns  0
     *      versionA = 4.16.1, versionB = 4.15.0 returns -1
     */ static compareVersions ( versionA , versionB ) { if ( versionA === versionB ) { return 0 ; } const versionANumber = ( versionA || '' ) . split ( '.' ) . filter ( ( x ) => ( /^[0-9]+$/ ) . exec ( x ) !== null ) ; const versionBNumber = ( versionB || '' ) . split ( '.' ) . filter ( ( x ) => ( /^[0-9]+$/ ) . exec ( x ) !== null ) ; for ( let i = 0 ; i < Math . max ( versionANumber . length , versionBNumber . length ) ; i ++ ) { const a = parseInt ( versionANumber [ i ] , 10 ) || 0 ; const b = parseInt ( versionBNumber [ i ] , 10 ) || 0 ; if ( a > b ) { return - 1 ; } if ( a < b ) { return 1 ; } } return 0 ; } static isDesktop ( ) { return Utils . isDesktopApp ( ) && ( Utils . compareVersions ( Utils . getDesktopVersion ( ) , '5.0.0' ) <= 0 ) ; } static getReadToken ( ) { const queryString = new URLSearchParams ( window . location . search ) ; const readToken = queryString . get ( 'r' ) || '' ; return readToken ; } static generateClassName ( conditions ) { return Object . entries ( conditions ) . map ( ( [ className , condition ] ) => ( condition ? className : '' ) ) . filter ( ( className ) => className !== '' ) . join ( ' ' ) ; } static buildOriginalPath ( workspaceId = '' , boardId = '' , viewId = '' , cardId = '' ) { let originalPath = '' ; if ( workspaceId ) { originalPath += `${ workspaceId }/` ; } if ( boardId ) { originalPath += `${ boardId }/` ; } if ( viewId ) { originalPath += `${ viewId }/` ; } if ( cardId ) { originalPath += `${ cardId }/` ; } return originalPath ; } } export { Utils , IDType } ;