Emoji favicons for the web
0
fork

Configure Feed

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

feat: add custom emoji delete buttom

+270 -98
+7 -3
source/background.ts
··· 34 34 if (!emoji) return; 35 35 36 36 try { 37 - await browserAPI.tabs.sendMessage(tabId, { emoji, shouldOverride }); 37 + await browserAPI.tabs.sendMessage(tabId, { 38 + emoji, 39 + shouldOverride, 40 + enableOverrideIndicator: settings.features.enableOverrideIndicator, 41 + }); 38 42 } catch (e) { 39 43 console.log(e); 40 44 } ··· 51 55 52 56 if (isSettingsV1(storage)) { 53 57 console.info('Version < 2 versions found', storage); 54 - console.info('Migrating to', settings); 55 58 settings = migrateStorageFromV1(storage); 59 + console.info('Migrating to', settings); 56 60 await browserAPI.storage.sync.remove(LEGACY_STORAGE_KEYS); 57 - await browserAPI.storage.sync.set(settings); 61 + await browserAPI.storage.sync.set({ settings }); 58 62 } else if ( 59 63 !storage?.settings || 60 64 Object.keys(storage.settings).length !==
+62
source/components/emoji_selector/components/custom_delete.tsx
··· 1 + /* @jsx h */ 2 + import type { EmojiMap } from '../../../models/emoji.ts'; 3 + import type { SetRoute } from '../types.ts'; 4 + import type { Settings } from '../../../models/settings.ts'; 5 + 6 + import { Fragment, h } from 'preact'; 7 + import { useCallback, useContext, useState } from 'preact/hooks'; 8 + 9 + import { SettingsContext } from '../../../models/settings.ts'; 10 + import { deleteEmoji, emoji } from '../../../models/emoji.ts'; 11 + import { createFaviconURLFromImage } from '../../../utilities/image_helpers.ts'; 12 + import Only from '../../only.tsx'; 13 + import { ROUTE } from '../types.ts'; 14 + import EmojiButton from './emoji_button.tsx'; 15 + 16 + export default function CustomDelete({ 17 + customEmojis, 18 + setRoute, 19 + }: { 20 + customEmojis: EmojiMap; 21 + setRoute: SetRoute; 22 + }) { 23 + const settings = useContext<BrowserStorage<Settings>>(SettingsContext); 24 + const { cache, saveToStorageBypassCache } = settings; 25 + return ( 26 + <div className='emoji-custom-upload'> 27 + <div classname='emoji-group'> 28 + {Object.keys(customEmojis) 29 + .map((name) => { 30 + const emoji = customEmojis[name]; 31 + return ( 32 + <EmojiButton 33 + className='emoji-selector-button' 34 + onClick={async () => { 35 + try { 36 + if (confirm(`Delete ${name}?`)) { 37 + await deleteEmoji(emoji); 38 + await saveToStorageBypassCache({ 39 + ...cache, 40 + customEmojiIds: cache.customEmojiIds 41 + .filter((desc) => desc !== emoji.description), 42 + }); 43 + setRoute(ROUTE.DEFAULT); 44 + } 45 + } catch (e) { 46 + confirm(e); 47 + } 48 + }} 49 + emoji={emoji} 50 + /> 51 + ); 52 + })} 53 + </div> 54 + <button 55 + type='button' 56 + onClick={useCallback(() => setRoute(ROUTE.DEFAULT), [setRoute])} 57 + > 58 + cancel 59 + </button> 60 + </div> 61 + ); 62 + }
+35 -28
source/components/emoji_selector/components/custom_upload.tsx
··· 1 1 /* @jsx h */ 2 + import type { SetRoute } from '../types.ts'; 2 3 3 4 import { Fragment, h } from 'preact'; 4 5 import { useCallback, useState } from 'preact/hooks'; 5 - import * as emoji from 'emoji'; 6 6 7 - import Only from '../../only.tsx'; 7 + import { emoji } from '../../../models/emoji.ts'; 8 8 import { createFaviconURLFromImage } from '../../../utilities/image_helpers.ts'; 9 - import type { SetSwitch } from '../types.ts'; 9 + import Only from '../../only.tsx'; 10 + import { ROUTE } from '../types.ts'; 10 11 11 12 export default function CustomUpload( 12 - { setIsCustom, submitCustomEmoji }: { 13 - setIsCustom: SetSwitch; 14 - submitCustomEmoji: ( 15 - name: string, 16 - image: string, 17 - type: string, 18 - ) => Promise<void>; 13 + { setRoute, submitCustomEmoji }: { 14 + setRoute: SetRoute; 15 + submitCustomEmoji: (name: string, image: string) => Promise<void>; 19 16 }, 20 17 ) { 21 18 const [image, setSelectedEmoji] = useState(''); 22 19 const [name, setName] = useState<string>(''); 23 - const exitIsCustomPage = useCallback(() => setIsCustom(false), [setIsCustom]); 24 - const submitCustomEmojiCb = useCallback(async () => { 25 - await submitCustomEmoji(name, image, 'image'); 26 - setIsCustom(false); 27 - }, [image, name, setIsCustom]); 28 - 29 - const updateName = useCallback((event: Event) => { 30 - setName((event?.target as HTMLInputElement)?.value || ''); 31 - }, [setName]); 32 20 33 21 const updateImage = useCallback(async (event: Event) => { 34 - const file = (event?.target as HTMLInputElement)?.files?.[0]; 35 - if (file?.name && !name) setName(file.name.match(/(.*)\..*$/)?.[1] || ''); 36 - const url = await createFaviconURLFromImage( 37 - URL.createObjectURL(file as Blob), 38 - ); 39 - setSelectedEmoji(url); 22 + if (event.target instanceof HTMLInputElement) { 23 + const file = event.target?.files?.[0]; 24 + if (file?.name && !name) setName(file.name.match(/(.*)\..*$/)?.[1] || ''); 25 + const url = await createFaviconURLFromImage( 26 + URL.createObjectURL(file as Blob), 27 + ); 28 + setSelectedEmoji(url); 29 + } 40 30 }, [setSelectedEmoji, setName, name]); 41 31 42 32 return ( ··· 58 48 name='Name' 59 49 placeholder='Name' 60 50 value={name} 61 - onChange={updateName} 51 + onChange={useCallback(({ target }: Event) => { 52 + if (target instanceof HTMLInputElement) { 53 + setName(target.value || ''); 54 + } 55 + }, [setName])} 62 56 /> 63 57 </div> 64 - <button type='button' onClick={submitCustomEmojiCb}>submit</button> 65 - <button type='button' onClick={exitIsCustomPage}>cancel</button> 58 + <button 59 + type='button' 60 + onClick={useCallback(() => submitCustomEmoji(name, image), [ 61 + name, 62 + image, 63 + ])} 64 + > 65 + submit 66 + </button> 67 + <button 68 + type='button' 69 + onClick={useCallback(() => setRoute(ROUTE.DEFAULT), [setRoute])} 70 + > 71 + cancel 72 + </button> 66 73 </div> 67 74 ); 68 75 }
+21 -10
source/components/emoji_selector/components/groups.tsx
··· 1 1 /* @jsx h */ 2 - import type { OnSelected, SetSwitch } from '../types.ts'; 2 + import type { OnSelected, SetRoute } from '../types.ts'; 3 3 import type { Emoji, EmojiGroup, EmojiGroups } from '../../../models/emoji.ts'; 4 4 5 5 import { Fragment, h } from 'preact'; 6 6 import { useCallback, useMemo } from 'preact/hooks'; 7 7 8 + import Only from '../../only.tsx'; 8 9 import EmojiButton from './emoji_button.tsx'; 10 + import { ROUTE } from '../types.ts'; 9 11 10 12 export default function Groups( 11 - { groupFilter, filter, onSelected, emojiGroups, setIsCustom }: { 13 + { groupFilter, filter, onSelected, emojiGroups, setRoute }: { 12 14 groupFilter: string; 13 15 filter: string; 14 16 onSelected: OnSelected; 15 17 emojiGroups: EmojiGroups; 16 - setIsCustom: SetSwitch; 18 + setRoute: SetRoute; 17 19 }, 18 20 ) { 19 21 const emojiFilter = useCallback((emoji: Emoji) => { ··· 59 61 <Fragment> 60 62 {shouldNotShowGroups ? '' : emojiGroupComponents} 61 63 {!emojiGroupComponents.length ? 'No Matches' : ''} 62 - {groupFilter === '' || groupFilter === 'Custom Emojis' 63 - ? ( 64 - <button type='button' onClick={() => setIsCustom(true)}> 65 - Add Custom Emoji 66 - </button> 67 - ) 68 - : ''} 64 + <Only if={groupFilter === '' || groupFilter === 'Custom Emojis'}> 65 + <button 66 + className='custom-emoji-button' 67 + type='button' 68 + onClick={() => setRoute(ROUTE.CREATE_CUSTOM)} 69 + > 70 + Add Custom Emoji 71 + </button> 72 + <button 73 + className='custom-emoji-button' 74 + type='button' 75 + onClick={() => setRoute(ROUTE.DELETE_CUSTOM)} 76 + > 77 + Remove Custom Emoji 78 + </button> 79 + </Only> 69 80 </Fragment> 70 81 ); 71 82 }
+27 -14
source/components/emoji_selector/components/popup.tsx
··· 1 1 /* @jsx h */ 2 2 import type { Emoji, EmojiGroup, EmojiMap } from '../../../models/emoji.ts'; 3 - import type { OnSelected, SetSwitch } from '../types.ts'; 3 + import type { OnSelected, Route, SetRoute, SetSwitch } from '../types.ts'; 4 4 import type { Ref } from 'preact'; 5 5 6 6 import { Fragment, h } from 'preact'; ··· 9 9 import { emoji, emojiGroups, emojiGroupsArray } from '../../../models/emoji.ts'; 10 10 import Groups from './groups.tsx'; 11 11 import CustomUpload from './custom_upload.tsx'; 12 + import CustomDelete from './custom_delete.tsx'; 13 + import { ROUTE } from '../types.ts'; 12 14 13 15 const POPUP_WIDTH = 350; 14 16 const BUTTON_HEIGHT = 32; 15 17 16 18 export default function Popup( 17 19 { 18 - isCustom, 20 + customEmojis, 19 21 isOpen, 20 22 onSelected, 21 23 popupRef, 22 - setIsCustom, 24 + route, 23 25 setIsOpen, 26 + setRoute, 24 27 submitCustomEmoji, 25 - customEmojis, 26 28 }: { 29 + customEmojis: EmojiMap; 27 30 isOpen: boolean; 28 - isCustom: boolean; 29 31 onSelected: OnSelected; 30 32 // deno-lint-ignore no-explicit-any 31 33 popupRef: Ref<any>; 32 - setIsCustom: SetSwitch; 34 + route: Route; 33 35 setIsOpen: SetSwitch; 34 - customEmojis: EmojiMap; 36 + setRoute: SetRoute; 35 37 submitCustomEmoji: ( 36 38 name: string, 37 39 image: string, ··· 50 52 51 53 if (!isOpen) return null; 52 54 53 - if (isCustom) { 55 + if (route === ROUTE.CREATE_CUSTOM) { 54 56 return ( 55 57 <div className='emoji-selector-popup' ref={popupRef}> 56 58 <CustomUpload 57 - setIsCustom={setIsCustom} 59 + setRoute={setRoute} 58 60 submitCustomEmoji={submitCustomEmoji} 59 61 /> 60 62 </div> 61 63 ); 62 64 } 63 65 66 + if (route === ROUTE.DELETE_CUSTOM) { 67 + return ( 68 + <div className='emoji-selector-popup' ref={popupRef}> 69 + <CustomDelete 70 + setRoute={setRoute} 71 + customEmojis={customEmojis} 72 + /> 73 + </div> 74 + ); 75 + } 76 + 64 77 return ( 65 78 <div className='emoji-selector-popup' ref={popupRef}> 66 79 <div className='emoji-header'> ··· 90 103 onInput={setFilter} 91 104 /> 92 105 </div> 93 - 94 106 <Groups 95 - setIsCustom={setIsCustom} 107 + setRoute={setRoute} 96 108 emojiGroups={allEmojis} 97 109 filter={filter} 98 110 groupFilter={groupFilter} ··· 106 118 const [filter, setFilter] = useState(initialValue); 107 119 108 120 const updateFilter = useCallback((e: Event) => { 109 - const filter = (e?.target as HTMLInputElement)?.value || ''; 110 - setFilter(filter); 111 - }, []); 121 + if (e.target instanceof HTMLInputElement) { 122 + setFilter(e.target?.value || ''); 123 + } 124 + }, [setFilter]); 112 125 113 126 return [filter, updateFilter]; 114 127 }
+43 -27
source/components/emoji_selector/mod.tsx
··· 2 2 import type { Emoji, EmojiMap } from '../../models/emoji.ts'; 3 3 import type { Settings } from '../../models/settings.ts'; 4 4 import type { BrowserStorage } from '../../hooks/use_browser_storage.ts'; 5 + import type { OnSelected, Route } from './types.ts'; 5 6 6 7 import { Fragment, h } from 'preact'; 7 8 import { ··· 28 29 import { SettingsContext } from '../../models/settings.ts'; 29 30 import EmojiButton from './components/emoji_button.tsx'; 30 31 import Popup from './components/popup.tsx'; 31 - import { OnSelected } from './types.ts'; 32 + import { ROUTE } from './types.ts'; 32 33 33 34 const defaultState = {}; 34 35 export default function EmojiSelector({ onSelected, emojiId }: { 35 36 emojiId?: string; 36 37 onSelected: OnSelected; 37 38 }) { 39 + const buttonRef = useRef<HTMLButtonElement>(); 38 40 const settings = useContext<BrowserStorage<Settings>>(SettingsContext); 39 41 const { cache, setCache, saveToStorageBypassCache } = settings; 42 + 40 43 const [customEmojis, setCustomEmojis] = useState({}); 44 + const [isOpen, setIsOpen] = useState<boolean>(false); 45 + const [route, setRoute] = useState<Route>(ROUTE.DEFAULT); 46 + const [selectedEmoji, setSelectedEmoji] = useState<Emoji>(DEFAULT_EMOJI); 47 + 41 48 useEffect(() => { 42 49 const currIds = Object.keys(customEmojis).sort(); 50 + if (currIds.length > cache.customEmojiIds.length) { 51 + const nextEmojis = {}; 52 + cache.customEmojiIds.forEach((id) => { 53 + nextEmojis[id] = customEmojis[id]; 54 + }); 55 + setCustomEmojis(nextEmojis); 56 + return; 57 + } 58 + 43 59 const matches = cache.customEmojiIds.sort() 44 60 .every((value, index) => currIds[index] === value); 45 61 46 - if (!matches) { 47 - (async function fetchEmojis() { 48 - setCustomEmojis(await getEmojis(cache.customEmojiIds)); 49 - })(); 50 - } 62 + if (matches) return; 63 + 64 + (async function fetchEmojis() { 65 + setCustomEmojis(await getEmojis(cache.customEmojiIds)); 66 + })(); 51 67 }, [cache.customEmojiIds]); 52 - 53 - const buttonRef = useRef<HTMLButtonElement>(); 54 - const [isOpen, setIsOpen] = useState<boolean>(false); 55 - const [isCustom, setIsCustom] = useState<boolean>(false); 56 - const [selectedEmoji, setSelectedEmoji] = useState<Emoji>(DEFAULT_EMOJI); 57 68 58 69 // For reverting to default state when list_items are deleted 59 70 useEffect(function updateStateWithNewValue() { ··· 71 82 return ( 72 83 <Fragment> 73 84 <EmojiButton 74 - emoji={selectedEmoji} 75 85 className='emoji-selector-button' 76 - ref={buttonRef} 86 + emoji={selectedEmoji} 77 87 onClick={useCallback(() => { 78 88 setIsOpen(!isOpen); 79 - setIsCustom(false); 89 + setRoute(ROUTE.DEFAULT); 80 90 }, [isOpen])} 91 + ref={buttonRef} 81 92 /> 82 93 <Popup 94 + customEmojis={customEmojis} 95 + isOpen={isOpen} 83 96 popupRef={useFocusObserver( 84 97 useCallback(() => { 85 98 setIsOpen(false); 86 - setIsCustom(false); 99 + setRoute(ROUTE.DEFAULT); 87 100 }, [setIsOpen]), 88 101 [buttonRef], 89 102 )} 90 - isOpen={isOpen} 91 - setIsOpen={setIsOpen} 92 - isCustom={isCustom} 93 - setIsCustom={setIsCustom} 94 - customEmojis={customEmojis} 95 - submitCustomEmoji={useCallback(async (description, url) => { 96 - await saveEmoji(createEmoji(description, url)); 97 - const customEmojiIds = Array.from( 98 - new Set(cache.customEmojiIds.concat(description)), 99 - ); 100 - await saveToStorageBypassCache({ ...cache, customEmojiIds }); 101 - }, [settings, cache.customEmojiIds])} 102 103 onSelected={useCallback((emoji: Emoji) => { 103 104 if (!isOpen) return; 104 105 onSelected(emoji); 105 106 setSelectedEmoji(emoji); 106 107 setIsOpen(false); 107 108 }, [isOpen, setIsOpen])} 109 + route={route} 110 + setIsOpen={setIsOpen} 111 + setRoute={setRoute} 112 + submitCustomEmoji={useCallback(async (description, url) => { 113 + try { 114 + await saveEmoji(createEmoji(description, url)); 115 + const customEmojiIds = Array.from( 116 + new Set(cache.customEmojiIds.concat(description)), 117 + ); 118 + await saveToStorageBypassCache({ ...cache, customEmojiIds }); 119 + setRoute(ROUTE.DEFAULT); 120 + } catch (e) { 121 + confirm(e); 122 + } 123 + }, [settings, cache.customEmojiIds])} 108 124 /> 109 125 </Fragment> 110 126 );
+12
source/components/emoji_selector/types.ts
··· 2 2 3 3 export type SetSwitch = (state: boolean) => void; 4 4 export type OnSelected = (emoji: Emoji) => void; 5 + export type SetRoute = (state: string) => void; 6 + 7 + export const ROUTE = { 8 + DEFAULT: 'DEFAULT', 9 + CREATE_CUSTOM: 'CREATE_CUSTOM', 10 + DELETE_CUSTOM: 'DELETE_CUSTOM', 11 + }; 12 + 13 + export type Route = 14 + | typeof ROUTE.DEFAULT 15 + | typeof ROUTE.CREATE_CUSTOM 16 + | typeof ROUTE.DELETE_CUSTOM;
+6 -5
source/components/list_input.tsx
··· 34 34 const choices = [ 35 35 'favioli.com', 36 36 'https://favioli.com', 37 - '/fa.ioli$/', 38 - '/favioli/', 37 + 'favioli', 38 + '/favioli.com$/i', 39 39 '/http:\\/\\//', 40 40 ]; 41 41 ··· 49 49 index, 50 50 }: ListInputProps) { 51 51 const onChangeMatcher = useCallback((e: Event) => { 52 - const matcher = (e.target as HTMLInputElement).value; 53 - const next = { emojiId: value?.emojiId || '', matcher }; 54 - addItem ? addItem(next) : updateItem(index, next); 52 + if (e.target instanceof HTMLInputElement) { 53 + const next = { emojiId: value?.emojiId || '', matcher: e.target.value }; 54 + addItem ? addItem(next) : updateItem(index, next); 55 + } 55 56 }, [index, value, updateItem, addItem]); 56 57 57 58 const onChangeEmoji = useCallback((selectedEmoji: Emoji) => {
+11 -6
source/content_script.ts
··· 11 11 import appendFaviconLink from './utilities/append_favicon_link.ts'; 12 12 import { parseRegExp } from './utilities/regex_utils.ts'; 13 13 14 - browserAPI.runtime.onMessage.addListener(({ emoji, shouldOverride }: { 15 - emoji: Emoji; 16 - shouldOverride: boolean; 17 - }) => { 18 - if (emoji) appendFaviconLink(emoji, { shouldOverride }); 19 - }); 14 + browserAPI.runtime.onMessage.addListener( 15 + ({ emoji, shouldOverride, enableOverrideIndicator }: { 16 + emoji: Emoji; 17 + shouldOverride: boolean; 18 + enableOverrideIndicator: boolean; 19 + }) => { 20 + if (emoji) { 21 + appendFaviconLink(emoji, { shouldOverride, enableOverrideIndicator }); 22 + } 23 + }, 24 + ); 20 25 21 26 /** 22 27 * Reload the webpage if new Favioli settings may have updated the favicon
+3 -2
source/hooks/use_selected_favicon.ts
··· 51 51 const selectedFaviconURL = useMemo((): string => { 52 52 if (!selectedEmoji) return ''; 53 53 const { imageURL, emoji } = selectedEmoji; 54 - return imageURL || createFaviconURLFromChar(emoji || ''); 55 - }, [selectedEmoji]); 54 + return imageURL || 55 + createFaviconURLFromChar(emoji || '', features.enableOverrideIndicator); 56 + }, [selectedEmoji, features.enableOverrideIndicator]); 56 57 57 58 if (!url || !settings) { 58 59 return {
+1
source/models/__fixtures__/settings_fixtures.ts
··· 94 94 enableFaviconAutofill: true, 95 95 enableOverrideAll: false, 96 96 enableSiteIgnore: true, 97 + enableOverrideIndicator: true, 97 98 }, 98 99 ignoreList: [ 99 100 createFavicon('hahahahh'),
+5
source/models/emoji.ts
··· 63 63 }; 64 64 } 65 65 66 + export async function deleteEmoji(emojiToDelete: Emoji): Promise<void> { 67 + const desc: string = emojiToDelete.description; 68 + await storage.sync.remove(getEmojiStorageId(desc)); 69 + } 70 + 66 71 export const getEmojiStorageId = (id: string) => `Custom Emoji: ${id}`; 67 72 const byDescription: EmojiMap = fromEntries( 68 73 emojis.map((emoji) => [emoji.description, emoji]),
+2
source/models/settings.ts
··· 23 23 enableFaviconAutofill: boolean; 24 24 enableSiteIgnore: boolean; 25 25 enableOverrideAll: boolean; 26 + enableOverrideIndicator: boolean; 26 27 }; 27 28 } 28 29 ··· 40 41 enableFaviconAutofill: true, 41 42 enableSiteIgnore: false, 42 43 enableOverrideAll: false, 44 + enableOverrideIndicator: false, 43 45 }, 44 46 }; 45 47
+1
source/models/storage_legacy.ts
··· 57 57 enableAutoselectorIncludeCountryFlags: false, 58 58 enableFaviconAutofill: true, 59 59 enableOverrideAll: legacySettings.overrideAll, 60 + enableOverrideIndicator: legacySettings.flagReplaced, 60 61 enableSiteIgnore: Boolean(legacySettings?.skips?.length), 61 62 }; 62 63
+7
source/pages/settings_page.tsx
··· 26 26 enableFaviconAutofill, 27 27 enableSiteIgnore, 28 28 enableOverrideAll, 29 + enableOverrideIndicator, 29 30 } = features; 30 31 31 32 const setFeature = useCallback((nextFeature: Target) => { ··· 80 81 </div> 81 82 </div> 82 83 </Only> 84 + <Checkbox 85 + name='enableOverrideIndicator' 86 + label={t('enableOverrideIndicatorLabel')} 87 + checked={enableOverrideIndicator} 88 + onChange={setFeature} 89 + /> 83 90 <button type='submit' children={t('saveLabel')} className='save' /> 84 91 </form> 85 92 );
+4 -2
source/utilities/append_favicon_link.ts
··· 11 11 12 12 interface Options { 13 13 shouldOverride?: boolean; 14 + enableOverrideIndicator?: boolean; 14 15 } 15 16 16 17 // Given an emoji string, append it to the document head ··· 18 19 emoji: Emoji, 19 20 options?: Options | void, 20 21 ) { 21 - const { shouldOverride = false } = options || {}; 22 + const { shouldOverride = false, enableOverrideIndicator = false } = options || 23 + {}; 22 24 const faviconURL = emoji.imageURL 23 25 ? emoji.imageURL 24 - : createFaviconURLFromChar(emoji.emoji || ''); 26 + : createFaviconURLFromChar(emoji.emoji || '', enableOverrideIndicator); 25 27 26 28 if (!faviconURL) return; 27 29 if (shouldOverride) removeAllFaviconLinks();
+1
source/utilities/i18n.ts
··· 10 10 11 11 const en: { [name: string]: string } = { 12 12 enableOverrideAllLabel: 'Override All Favicons', 13 + enableOverrideIndicatorLabel: 'Indicate Overridden Favicons', 13 14 enableFaviconAutofillDesc: 14 15 'If a website doesn\'t have a favicon, Favioli will automatically create one for it using an emoji.', 15 16 enableFaviconAutofillLabel: 'Enable Autofill',
+18 -1
source/utilities/image_helpers.ts
··· 27 27 * Heavily inspired by emoji-favicon-toolkit 28 28 * @source https://github.com/eligrey/emoji-favicon-toolkit/blob/master/src/emoji-favicon-toolkit.ts 29 29 */ 30 - export function createFaviconURLFromChar(char: string): string { 30 + export function createFaviconURLFromChar( 31 + char: string, 32 + showIndicator: boolean = false, 33 + ): string { 31 34 if (!char || !ctx) return ''; 32 35 33 36 // Calculate sizing ··· 42 45 ctx.save(); 43 46 ctx.scale(scale, scale); 44 47 ctx.fillText(char, centerScaled, centerScaled + VERTICAL_OFFSET); 48 + 49 + if (showIndicator) { 50 + const FLAG_SIZE = 30; 51 + ctx.beginPath(); 52 + ctx.arc( 53 + ICON_SIZE - FLAG_SIZE, 54 + ICON_SIZE - FLAG_SIZE, 55 + FLAG_SIZE, 56 + 0, 57 + 2 * Math.PI, 58 + ); 59 + ctx.fillStyle = 'red'; 60 + ctx.fill(); 61 + } 45 62 46 63 ctx.restore(); 47 64 return canvas.toDataURL('image/png');
+4
static/styles/shared.css
··· 212 212 height: 100%; 213 213 } 214 214 215 + .custom-emoji-button { 216 + margin: 5px; 217 + } 218 + 215 219 .emoji-custom-upload button { 216 220 margin: 5px; 217 221 }