allow user display names to contain custom emoji (#448)
* allow user display names to contain custom emoji fixes #445 * fix tests * fix focus issue
This commit is contained in:
		
							parent
							
								
									c660c7d3a3
								
							
						
					
					
						commit
						350667e5df
					
				
					 20 changed files with 117 additions and 37 deletions
				
			
		
							
								
								
									
										7
									
								
								routes/_api/updateCredentials.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								routes/_api/updateCredentials.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | import { WRITE_TIMEOUT, patch } from '../_utils/ajax' | ||||||
|  | import { auth, basename } from './utils' | ||||||
|  | 
 | ||||||
|  | export async function updateCredentials (instanceName, accessToken, accountData) { | ||||||
|  |   let url = `${basename(instanceName)}/api/v1/accounts/update_credentials` | ||||||
|  |   return patch(url, accountData, auth(accessToken), {timeout: WRITE_TIMEOUT}) | ||||||
|  | } | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
|   <Avatar account={verifyCredentials} size="small"/> |   <Avatar account={verifyCredentials} size="small"/> | ||||||
| </a> | </a> | ||||||
| <a class="compose-box-display-name" href="/accounts/{verifyCredentials.id}"> | <a class="compose-box-display-name" href="/accounts/{verifyCredentials.id}"> | ||||||
|   {verifyCredentials.display_name || verifyCredentials.acct} |   <AccountDisplayName account={verifyCredentials} /> | ||||||
| </a> | </a> | ||||||
| <span class="compose-box-handle"> | <span class="compose-box-handle"> | ||||||
|   {'@' + verifyCredentials.acct} |   {'@' + verifyCredentials.acct} | ||||||
|  | @ -51,9 +51,12 @@ | ||||||
| <script> | <script> | ||||||
|   import Avatar from '../Avatar.html' |   import Avatar from '../Avatar.html' | ||||||
|   import { store } from '../../_store/store' |   import { store } from '../../_store/store' | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
|  | 
 | ||||||
|   export default { |   export default { | ||||||
|     components: { |     components: { | ||||||
|       Avatar |       Avatar, | ||||||
|  |       AccountDisplayName | ||||||
|     }, |     }, | ||||||
|     store: () => store, |     store: () => store, | ||||||
|     computed: { |     computed: { | ||||||
|  |  | ||||||
|  | @ -12,7 +12,7 @@ | ||||||
|           account={item} |           account={item} | ||||||
|         /> |         /> | ||||||
|         <span class="compose-autosuggest-list-display-name"> |         <span class="compose-autosuggest-list-display-name"> | ||||||
|             {item.display_name || item.acct} |             <AccountDisplayName account={item} /> | ||||||
|         </span> |         </span> | ||||||
|         <span class="compose-autosuggest-list-username"> |         <span class="compose-autosuggest-list-username"> | ||||||
|             {'@' + item.acct} |             {'@' + item.acct} | ||||||
|  | @ -99,6 +99,7 @@ | ||||||
| <script> | <script> | ||||||
|   import Avatar from '../Avatar.html' |   import Avatar from '../Avatar.html' | ||||||
|   import { store } from '../../_store/store' |   import { store } from '../../_store/store' | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
| 
 | 
 | ||||||
|   export default { |   export default { | ||||||
|     store: () => store, |     store: () => store, | ||||||
|  | @ -110,7 +111,8 @@ | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     components: { |     components: { | ||||||
|       Avatar |       Avatar, | ||||||
|  |       AccountDisplayName | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								routes/_components/profile/AccountDisplayName.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								routes/_components/profile/AccountDisplayName.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | <span class="account-display-name">{@html massagedAccountName }</span> | ||||||
|  | <style> | ||||||
|  |   .account-display-name { | ||||||
|  |     pointer-events: none; /* allows focus to work correctly, focus on the parent only */ | ||||||
|  |   } | ||||||
|  | </style> | ||||||
|  | <script> | ||||||
|  |   import { emojifyText } from '../../_utils/emojifyText' | ||||||
|  |   import { store } from '../../_store/store' | ||||||
|  |   import escapeHtml from 'escape-html' | ||||||
|  | 
 | ||||||
|  |   export default { | ||||||
|  |     store: () => store, | ||||||
|  |     computed: { | ||||||
|  |       emojis: ({ account }) => (account.emojis || []), | ||||||
|  |       accountName: ({ account }) => (account.display_name || account.username), | ||||||
|  |       massagedAccountName: ({ accountName, emojis, $autoplayGifs }) => { | ||||||
|  |         accountName = escapeHtml(accountName) | ||||||
|  |         return emojifyText(accountName, emojis, $autoplayGifs) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | </script> | ||||||
|  | @ -7,7 +7,7 @@ | ||||||
|                 normalIconColor="true" |                 normalIconColor="true" | ||||||
|                 ariaLabel="{account.display_name || account.acct} (opens in new window)" |                 ariaLabel="{account.display_name || account.acct} (opens in new window)" | ||||||
|   > |   > | ||||||
|     {account.display_name || account.acct} |     <AccountDisplayName {account} /> | ||||||
|   </ExternalLink> |   </ExternalLink> | ||||||
| </div> | </div> | ||||||
| <div class="account-profile-username"> | <div class="account-profile-username"> | ||||||
|  | @ -80,11 +80,13 @@ | ||||||
| <script> | <script> | ||||||
|   import Avatar from '../Avatar.html' |   import Avatar from '../Avatar.html' | ||||||
|   import ExternalLink from '../ExternalLink.html' |   import ExternalLink from '../ExternalLink.html' | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
| 
 | 
 | ||||||
|   export default { |   export default { | ||||||
|     components: { |     components: { | ||||||
|       Avatar, |       Avatar, | ||||||
|       ExternalLink |       ExternalLink, | ||||||
|  |       AccountDisplayName | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
|   <div class="search-result-account"> |   <div class="search-result-account"> | ||||||
|     <Avatar {account} size="small" className="search-result-account-avatar"/> |     <Avatar {account} size="small" className="search-result-account-avatar"/> | ||||||
|     <div class="search-result-account-name"> |     <div class="search-result-account-name"> | ||||||
|       {account.display_name || account.acct} |       <AccountDisplayName {account} /> | ||||||
|     </div> |     </div> | ||||||
|     <div class="search-result-account-username"> |     <div class="search-result-account-username"> | ||||||
|       {'@' + account.acct} |       {'@' + account.acct} | ||||||
|  | @ -71,6 +71,7 @@ | ||||||
|   import Avatar from '../Avatar.html' |   import Avatar from '../Avatar.html' | ||||||
|   import SearchResult from './SearchResult.html' |   import SearchResult from './SearchResult.html' | ||||||
|   import IconButton from '../IconButton.html' |   import IconButton from '../IconButton.html' | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
| 
 | 
 | ||||||
|   export default { |   export default { | ||||||
|     data: () => ({ |     data: () => ({ | ||||||
|  | @ -89,7 +90,8 @@ | ||||||
|     components: { |     components: { | ||||||
|       Avatar, |       Avatar, | ||||||
|       SearchResult, |       SearchResult, | ||||||
|       IconButton |       IconButton, | ||||||
|  |       AccountDisplayName | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|    title="{'@' + originalAccount.acct}" |    title="{'@' + originalAccount.acct}" | ||||||
|    focus-key={focusKey} |    focus-key={focusKey} | ||||||
| > | > | ||||||
|   {originalAccount.display_name || originalAccount.username} |   <AccountDisplayName account={originalAccount} /> | ||||||
| </a> | </a> | ||||||
| <style> | <style> | ||||||
|   .status-author-name { |   .status-author-name { | ||||||
|  | @ -34,9 +34,14 @@ | ||||||
| 
 | 
 | ||||||
| </style> | </style> | ||||||
| <script> | <script> | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
|  | 
 | ||||||
|   export default { |   export default { | ||||||
|     computed: { |     computed: { | ||||||
|       focusKey: ({ uuid }) => `status-author-name-${uuid}` |       focusKey: ({ uuid }) => `status-author-name-${uuid}` | ||||||
|  |     }, | ||||||
|  |     components: { | ||||||
|  |       AccountDisplayName | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  | @ -21,14 +21,6 @@ | ||||||
|     display: block; |     display: block; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   :global(.status-content .status-emoji) { |  | ||||||
|     width: 1.4em; |  | ||||||
|     height: 1.4em; |  | ||||||
|     margin: -0.1em 0; |  | ||||||
|     object-fit: contain; |  | ||||||
|     vertical-align: middle; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   :global(.status-content p) { |   :global(.status-content p) { | ||||||
|     margin: 0 0 20px; |     margin: 0 0 20px; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ | ||||||
|          class="status-header-author" |          class="status-header-author" | ||||||
|          title="{'@' + account.acct}" |          title="{'@' + account.acct}" | ||||||
|          focus-key={focusKey} > |          focus-key={focusKey} > | ||||||
|         {account.display_name || account.username} |         <AccountDisplayName {account} /> | ||||||
|       </a> |       </a> | ||||||
|     {/if} |     {/if} | ||||||
| 
 | 
 | ||||||
|  | @ -103,10 +103,12 @@ | ||||||
| </style> | </style> | ||||||
| <script> | <script> | ||||||
|   import Avatar from '../Avatar.html' |   import Avatar from '../Avatar.html' | ||||||
|  |   import AccountDisplayName from '../profile/AccountDisplayName.html' | ||||||
| 
 | 
 | ||||||
|   export default { |   export default { | ||||||
|     components: { |     components: { | ||||||
|       Avatar |       Avatar, | ||||||
|  |       AccountDisplayName | ||||||
|     }, |     }, | ||||||
|     computed: { |     computed: { | ||||||
|       focusKey: ({ uuid }) => `status-header-${uuid}`, |       focusKey: ({ uuid }) => `status-header-${uuid}`, | ||||||
|  |  | ||||||
|  | @ -16,14 +16,6 @@ | ||||||
|     margin: 10px 5px; |     margin: 10px 5px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   :global(.status-spoiler .status-emoji) { |  | ||||||
|     width: 1.4em; |  | ||||||
|     height: 1.4em; |  | ||||||
|     margin: -0.1em 0; |  | ||||||
|     object-fit: contain; |  | ||||||
|     vertical-align: middle; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   .status-spoiler.status-in-own-thread { |   .status-spoiler.status-in-own-thread { | ||||||
|     font-size: 1.3em; |     font-size: 1.3em; | ||||||
|     margin: 20px 5px 10px; |     margin: 20px 5px 10px; | ||||||
|  |  | ||||||
|  | @ -9,7 +9,9 @@ | ||||||
|                     href={verifyCredentials.url}> |                     href={verifyCredentials.url}> | ||||||
|         {'@' + verifyCredentials.acct} |         {'@' + verifyCredentials.acct} | ||||||
|       </ExternalLink> |       </ExternalLink> | ||||||
|       <span class="acct-display-name">{verifyCredentials.display_name || verifyCredentials.acct}</span> |       <span class="acct-display-name"> | ||||||
|  |         <AccountDisplayName account={verifyCredentials} /> | ||||||
|  |       </span> | ||||||
|     </div> |     </div> | ||||||
|     <h2>Theme:</h2> |     <h2>Theme:</h2> | ||||||
|     <form class="theme-chooser" aria-label="Choose a theme"> |     <form class="theme-chooser" aria-label="Choose a theme"> | ||||||
|  | @ -103,6 +105,7 @@ | ||||||
|     updateVerifyCredentialsForInstance |     updateVerifyCredentialsForInstance | ||||||
|   } from '../../../_actions/instances' |   } from '../../../_actions/instances' | ||||||
|   import { themes } from '../../../_static/themes' |   import { themes } from '../../../_static/themes' | ||||||
|  |   import AccountDisplayName from '../../../_components/profile/AccountDisplayName.html' | ||||||
| 
 | 
 | ||||||
|   export default { |   export default { | ||||||
|     async oncreate () { |     async oncreate () { | ||||||
|  | @ -148,7 +151,8 @@ | ||||||
|     components: { |     components: { | ||||||
|       SettingsLayout, |       SettingsLayout, | ||||||
|       ExternalLink, |       ExternalLink, | ||||||
|       Avatar |       Avatar, | ||||||
|  |       AccountDisplayName | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| </script> | </script> | ||||||
|  | @ -39,7 +39,7 @@ async function _fetch (url, fetchOptions, options) { | ||||||
|   return throwErrorIfInvalidResponse(response) |   return throwErrorIfInvalidResponse(response) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function _putOrPost (method, url, body, headers, options) { | async function _putOrPostOrPatch (method, url, body, headers, options) { | ||||||
|   let fetchOptions = makeFetchOptions(method, headers) |   let fetchOptions = makeFetchOptions(method, headers) | ||||||
|   if (body) { |   if (body) { | ||||||
|     if (body instanceof FormData) { |     if (body instanceof FormData) { | ||||||
|  | @ -53,11 +53,15 @@ async function _putOrPost (method, url, body, headers, options) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function put (url, body, headers, options) { | export async function put (url, body, headers, options) { | ||||||
|   return _putOrPost('PUT', url, body, headers, options) |   return _putOrPostOrPatch('PUT', url, body, headers, options) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function post (url, body, headers, options) { | export async function post (url, body, headers, options) { | ||||||
|   return _putOrPost('POST', url, body, headers, options) |   return _putOrPostOrPatch('POST', url, body, headers, options) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function patch (url, body, headers, options) { | ||||||
|  |   return _putOrPostOrPatch('PATCH', url, body, headers, options) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function get (url, headers, options) { | export async function get (url, headers, options) { | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ export function emojifyText (text, emojis, autoplayGifs) { | ||||||
|       text = replaceAll( |       text = replaceAll( | ||||||
|         text, |         text, | ||||||
|         shortcodeWithColons, |         shortcodeWithColons, | ||||||
|         `<img class="status-emoji" draggable="false" src="${urlToUse}"
 |         `<img class="inline-custom-emoji" draggable="false" src="${urlToUse}"
 | ||||||
|                     alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />` |                     alt="${shortcodeWithColons}" title="${shortcodeWithColons}" />` | ||||||
|       ) |       ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -197,4 +197,13 @@ textarea { | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   clip: rect(0, 0, 0, 0); |   clip: rect(0, 0, 0, 0); | ||||||
|   border: 0; |   border: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* this gets injected as raw HTML, so it's easiest to just define it in global.scss */ | ||||||
|  | .inline-custom-emoji { | ||||||
|  |   width: 1.4em; | ||||||
|  |   height: 1.4em; | ||||||
|  |   margin: -0.1em 0; | ||||||
|  |   object-fit: contain; | ||||||
|  |   vertical-align: middle; | ||||||
| } | } | ||||||
|  | @ -17,7 +17,7 @@ | ||||||
|   <style> |   <style> | ||||||
| /* auto-generated w/ build-sass.js */ | /* auto-generated w/ build-sass.js */ | ||||||
| body{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1)} | body{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1)} | ||||||
| body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:42px;left:0;right:0;bottom:0}@media (max-width: 991px){.container{top:52px}}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0} | body{margin:0;font-family:system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent}.container{overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;will-change:transform;position:absolute;top:42px;left:0;right:0;bottom:0}@media (max-width: 991px){.container{top:52px}}@media (max-width: 767px){.container{top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.inline-custom-emoji{width:1.4em;height:1.4em;margin:-0.1em 0;object-fit:contain;vertical-align:middle} | ||||||
| body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline,body.theme-ozark.offline,body.theme-cobalt.offline,body.theme-sorcery.offline{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1)} | body.offline,body.theme-hotpants.offline,body.theme-majesty.offline,body.theme-oaken.offline,body.theme-scarlet.offline,body.theme-seafoam.offline,body.theme-gecko.offline,body.theme-ozark.offline,body.theme-cobalt.offline,body.theme-sorcery.offline{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1)} | ||||||
| 
 | 
 | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import { postStatus } from '../routes/_api/statuses' | ||||||
| import { deleteStatus } from '../routes/_api/delete' | import { deleteStatus } from '../routes/_api/delete' | ||||||
| import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests' | import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests' | ||||||
| import { followAccount, unfollowAccount } from '../routes/_api/follow' | import { followAccount, unfollowAccount } from '../routes/_api/follow' | ||||||
|  | import { updateCredentials } from '../routes/_api/updateCredentials' | ||||||
| 
 | 
 | ||||||
| global.fetch = fetch | global.fetch = fetch | ||||||
| global.File = FileApi.File | global.File = FileApi.File | ||||||
|  | @ -46,3 +47,7 @@ export async function followAs (username, userToFollow) { | ||||||
| export async function unfollowAs (username, userToFollow) { | export async function unfollowAs (username, userToFollow) { | ||||||
|   return unfollowAccount(instanceName, users[username].accessToken, users[userToFollow].id) |   return unfollowAccount(instanceName, users[username].accessToken, users[userToFollow].id) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export async function updateUserDisplayNameAs (username, displayName) { | ||||||
|  |   return updateCredentials(instanceName, users[username].accessToken, {display_name: displayName}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -37,7 +37,7 @@ test('converts external links in profiles', async t => { | ||||||
|     .hover(getNthStatus(0)) |     .hover(getNthStatus(0)) | ||||||
|     .navigateTo('/accounts/4') |     .navigateTo('/accounts/4') | ||||||
|     .expect(getUrl()).contains('/accounts/4') |     .expect(getUrl()).contains('/accounts/4') | ||||||
|     .expect($('.account-profile-name').innerText).eql('External Lonk') |     .expect($('.account-profile-name').innerText).contains('External Lonk') | ||||||
|     .expect($('.account-profile-name a').getAttribute('href')).eql('http://localhost:3000/@ExternalLinks') |     .expect($('.account-profile-name a').getAttribute('href')).eql('http://localhost:3000/@ExternalLinks') | ||||||
|     .expect($('.account-profile-name a').getAttribute('rel')).eql('nofollow noopener') |     .expect($('.account-profile-name a').getAttribute('rel')).eql('nofollow noopener') | ||||||
|     .expect(getAnchorInProfile(0).getAttribute('href')).eql('https://joinmastodon.org') |     .expect(getAnchorInProfile(0).getAttribute('href')).eql('https://joinmastodon.org') | ||||||
|  |  | ||||||
|  | @ -44,9 +44,9 @@ test('content warnings can have emoji', async t => { | ||||||
|     .typeText(composeContentWarning, 'can you feel the :blobpats: tonight') |     .typeText(composeContentWarning, 'can you feel the :blobpats: tonight') | ||||||
|     .click(composeButton) |     .click(composeButton) | ||||||
|     .expect(getNthStatus(0).innerText).contains('can you feel the', {timeout: 30000}) |     .expect(getNthStatus(0).innerText).contains('can you feel the', {timeout: 30000}) | ||||||
|     .expect($(`${getNthStatusSelector(0)} .status-spoiler img.status-emoji`).getAttribute('alt')).eql(':blobpats:') |     .expect($(`${getNthStatusSelector(0)} .status-spoiler img.inline-custom-emoji`).getAttribute('alt')).eql(':blobpats:') | ||||||
|     .click(getNthShowOrHideButton(0)) |     .click(getNthShowOrHideButton(0)) | ||||||
|     .expect($(`${getNthStatusSelector(0)} .status-content img.status-emoji`).getAttribute('alt')).eql(':blobnom:') |     .expect($(`${getNthStatusSelector(0)} .status-content img.inline-custom-emoji`).getAttribute('alt')).eql(':blobnom:') | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| test('no XSS in content warnings or text', async t => { | test('no XSS in content warnings or text', async t => { | ||||||
|  |  | ||||||
							
								
								
									
										27
									
								
								tests/spec/118-display-name-custom-emoji.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tests/spec/118-display-name-custom-emoji.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | import { loginAsFoobar } from '../roles' | ||||||
|  | import { displayNameInComposeBox, getNthStatusSelector, getUrl, sleep } from '../utils' | ||||||
|  | import { updateUserDisplayNameAs } from '../serverActions' | ||||||
|  | import { Selector as $ } from 'testcafe' | ||||||
|  | 
 | ||||||
|  | fixture`118-display-name-custom-emoji.js` | ||||||
|  |   .page`http://localhost:4002` | ||||||
|  | 
 | ||||||
|  | test('Can put custom emoji in display name', async t => { | ||||||
|  |   await updateUserDisplayNameAs('foobar', 'foobar :blobpats:') | ||||||
|  |   await sleep(1000) | ||||||
|  |   await loginAsFoobar(t) | ||||||
|  |   await t | ||||||
|  |     .expect(displayNameInComposeBox.innerText).eql('foobar ') | ||||||
|  |     .expect($('.compose-box-display-name img').getAttribute('alt')).eql(':blobpats:') | ||||||
|  |     .click(displayNameInComposeBox) | ||||||
|  |     .expect(getUrl()).contains('/accounts/2') | ||||||
|  |     .expect($(`${getNthStatusSelector(0)} .status-author-name img`).getAttribute('alt')).eql(':blobpats:') | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | test('Cannot XSS using display name HTML', async t => { | ||||||
|  |   await updateUserDisplayNameAs('foobar', '<script>alert("pwn")</script>') | ||||||
|  |   await sleep(1000) | ||||||
|  |   await loginAsFoobar(t) | ||||||
|  |   await t | ||||||
|  |     .expect(displayNameInComposeBox.innerText).eql('<script>alert("pwn")</script>') | ||||||
|  | }) | ||||||
|  | @ -40,6 +40,7 @@ export const mastodonLogInButton = $('button[type="submit"]') | ||||||
| export const followsButton = $('.account-profile-details > *:nth-child(2)') | export const followsButton = $('.account-profile-details > *:nth-child(2)') | ||||||
| export const followersButton = $('.account-profile-details > *:nth-child(3)') | export const followersButton = $('.account-profile-details > *:nth-child(3)') | ||||||
| export const avatarInComposeBox = $('.compose-box-avatar') | export const avatarInComposeBox = $('.compose-box-avatar') | ||||||
|  | export const displayNameInComposeBox = $('.compose-box-display-name') | ||||||
| 
 | 
 | ||||||
| export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({ | export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({ | ||||||
|   innerCount: el => parseInt(el.innerText, 10) |   innerCount: el => parseInt(el.innerText, 10) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue