Emoji favicons for the web
0
fork

Configure Feed

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

fix: store favicons by id, instead of emoji data

+149 -128
+7 -3
source/background.ts
··· 13 13 migrateFromV1, 14 14 STORAGE_KEYS, 15 15 } from './utilities/settings.ts'; 16 - import FaviconData from './utilities/favicon_data.ts'; 16 + import { FaviconData, getEmojiFromFavicon } from './utilities/favicon_data.ts'; 17 17 import Autoselector from './utilities/autoselector.ts'; 18 18 19 19 let settings: Settings = DEFAULT_SETTINGS; ··· 28 28 try { 29 29 const [favicon, shouldOverride] = selectFavicon(tab.url, settings) || []; 30 30 const overrideText = shouldOverride ? 'Override' : 'Append'; 31 - console.log(`${overrideText} favicon, tab ${tabId}:`, favicon); 31 + console.info(`${overrideText} favicon, tab ${tabId}:`, favicon); 32 32 if (favicon && tabId) { 33 - await browserAPI.tabs.sendMessage(tabId, { favicon, shouldOverride }); 33 + const customEmojis = settings?.emojiDatabase?.customEmojis || []; 34 + const emoji = getEmojiFromFavicon(favicon, { customEmojis }); 35 + if (emoji) { 36 + await browserAPI.tabs.sendMessage(tabId, { emoji, shouldOverride }); 37 + } 34 38 } 35 39 } catch (e) { 36 40 console.log(e);
+1 -1
source/components/emoji_selector/components/custom_upload.tsx
··· 6 6 7 7 import Only from '../../only.tsx'; 8 8 import { createFaviconURLFromImage } from '../../../utilities/create_favicon_url.ts'; 9 - import { SetSwitch } from '../types.ts'; 9 + import type { SetSwitch } from '../types.ts'; 10 10 11 11 export default function CustomUpload( 12 12 { setIsCustom, submitCustomEmoji }: {
+7 -2
source/components/emoji_selector/components/groups.tsx
··· 1 1 /* @jsx h */ 2 + import type { OnSelected, SetSwitch } from '../types.ts'; 3 + import type { Emoji } from '../../../utilities/emoji.ts'; 4 + import type { 5 + EmojiGroup, 6 + EmojiGroups, 7 + } from '../../../utilities/favicon_data.ts'; 8 + 2 9 import { Fragment, h } from 'preact'; 3 10 import { useCallback, useMemo } from 'preact/hooks'; 4 11 5 - import { Emoji } from '../../../utilities/emoji.ts'; 6 - import { EmojiGroup, EmojiGroups, OnSelected, SetSwitch } from '../types.ts'; 7 12 import EmojiButton from './emoji_button.tsx'; 8 13 9 14 export default function Groups(
+7 -3
source/components/emoji_selector/components/popup.tsx
··· 1 1 /* @jsx h */ 2 + import type { Emoji } from '../../../utilities/emoji.ts'; 3 + import type { EmojiGroup } from '../../../utilities/favicon_data.ts'; 2 4 3 5 import { Fragment, h } from 'preact'; 4 6 import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; 5 7 import * as emoji from 'emoji'; 6 8 7 - import { Emoji } from '../../../utilities/emoji.ts'; 8 - import { EmojiGroup, OnSelected, SetSwitch } from '../types.ts'; 9 - import { emojiGroups, emojiGroupsArray } from '../constants.ts'; 9 + import { OnSelected, SetSwitch } from '../types.ts'; 10 + import { 11 + emojiGroups, 12 + emojiGroupsArray, 13 + } from '../../../utilities/favicon_data.ts'; 10 14 import Groups from './groups.tsx'; 11 15 import CustomUpload from './custom_upload.tsx'; 12 16
-32
source/components/emoji_selector/constants.ts
··· 1 - import { Emoji } from '../../utilities/emoji.ts'; 2 - import type { EmojiGroups } from './types.ts'; 3 - 4 - import * as emoji from 'emoji'; 5 - 6 - const emojis = emoji.all(); 7 - 8 - export const emojiGroups: EmojiGroups = {}; 9 - 10 - emojis.forEach((emoji) => { 11 - if (!emojiGroups[emoji.group]) { 12 - emojiGroups[emoji.group] = { 13 - name: emoji.group, 14 - emojis: [emoji], 15 - representativeEmoji: emoji.emoji, 16 - }; 17 - } else { 18 - emojiGroups[emoji.group].emojis.push(emoji); 19 - } 20 - }); 21 - 22 - emojiGroups['Custom Emojis'] = { 23 - name: 'Custom Emojis', 24 - emojis: [], 25 - representativeEmoji: '*', 26 - }; 27 - 28 - export const emojiGroupsArray = Object.keys(emojiGroups).map((name) => 29 - emojiGroups[name] 30 - ); 31 - 32 - export const DEFAULT_EMOJI = Object.freeze(emoji.infoByCode('😀') as Emoji);
+1 -1
source/components/emoji_selector/mod.tsx
··· 5 5 import * as emoji from 'emoji'; 6 6 7 7 import useFocusObserver from '../../hooks/use_focus_observer.ts'; 8 - import { DEFAULT_EMOJI } from './constants.ts'; 8 + import { DEFAULT_EMOJI } from '../../utilities/favicon_data.ts'; 9 9 import { OnSelected } from './types.ts'; 10 10 import { createCustomEmoji, Emoji } from '../../utilities/emoji.ts'; 11 11
-10
source/components/emoji_selector/types.ts
··· 3 3 export type SetSwitch = (state: boolean) => void; 4 4 5 5 export type OnSelected = (emoji: Emoji) => void; 6 - 7 - export interface EmojiGroup { 8 - name: string; 9 - representativeEmoji: string; 10 - emojis: Emoji[]; 11 - } 12 - 13 - export interface EmojiGroups { 14 - [name: string]: EmojiGroup; 15 - }
+1 -1
source/components/list.tsx
··· 5 5 import { h } from 'preact'; 6 6 import { useRef } from 'preact/hooks'; 7 7 8 - import FaviconData from '../utilities/favicon_data.ts'; 8 + import { FaviconData } from '../utilities/favicon_data.ts'; 9 9 import ListInput from './list_input.tsx'; 10 10 11 11 export interface ListProps<Type> {
+10 -7
source/components/list_input.tsx
··· 8 8 9 9 import { StorageContext } from '../hooks/use_browser_storage.ts'; 10 10 import { createCustomEmoji, Emoji } from '../utilities/emoji.ts'; 11 - import FaviconData from '../utilities/favicon_data.ts'; 11 + import { FaviconData, getEmojiFromFavicon } from '../utilities/favicon_data.ts'; 12 12 import { isRegexString } from '../utilities/predicates.ts'; 13 13 import EmojiSelector from './emoji_selector/mod.tsx'; 14 14 import Only from './only.tsx'; ··· 45 45 const { cache, saveToStorage } = storage; 46 46 const { customEmojis = {}, frequentlyUsed = [] } = cache?.emojiDatabase || {}; 47 47 48 - const onChangeInput = useCallback((e: Event) => { 48 + const onChangeMatcher = useCallback((e: Event) => { 49 49 const matcher = (e.target as HTMLInputElement).value; 50 - const next = new FaviconData(value?.emoji, matcher); 50 + const next = { id: value?.id || '', matcher }; 51 51 addItem ? addItem(next) : updateItem(index, next); 52 52 }, [index, value, updateItem, addItem]); 53 53 54 54 const onChangeEmoji = useCallback((selectedEmoji: Emoji) => { 55 - const next = new FaviconData(selectedEmoji, value?.matcher); 55 + const next = { 56 + id: selectedEmoji.description, 57 + matcher: value?.matcher || '', 58 + }; 56 59 addItem ? addItem(next) : updateItem(index, next); 57 60 }, [index, value, updateItem, addItem]); 58 61 ··· 67 70 <input 68 71 autoFocus={autoFocus} 69 72 className='filter' 70 - onInput={onChangeInput} 71 - onChange={onChangeInput} 73 + onInput={onChangeMatcher} 74 + onChange={onChangeMatcher} 72 75 placeholder={placeholder} 73 76 style={{ color }} 74 77 value={value?.matcher || ''} ··· 76 79 77 80 <Only if={type === 'FAVICON'}> 78 81 <EmojiSelector 79 - value={value?.emoji} 82 + value={getEmojiFromFavicon(value, { customEmojis })} 80 83 onSelected={onChangeEmoji} 81 84 customEmojis={customEmojis} 82 85 frequentlyUsed={frequentlyUsed}
+5 -8
source/content_script.ts
··· 7 7 */ 8 8 import browserAPI from 'browser'; 9 9 10 - import type FaviconData from './utilities/favicon_data.ts'; 10 + import type { Emoji } from './utilities/emoji.ts'; 11 11 import { appendFaviconLink } from './utilities/favicon_helpers.ts'; 12 12 13 13 /** ··· 57 57 return (new RegExp(val)).test(location.href); 58 58 } 59 59 60 - browserAPI.runtime.onMessage.addListener(({ 61 - favicon, 62 - shouldOverride, 63 - }: { 64 - favicon: FaviconData; 60 + browserAPI.runtime.onMessage.addListener(({ emoji, shouldOverride }: { 61 + emoji: Emoji; 65 62 shouldOverride: boolean; 66 63 }) => { 67 - if (favicon.emoji) { 68 - appendFaviconLink(favicon.emoji, { shouldOverride }); 64 + if (emoji) { 65 + appendFaviconLink(emoji, { shouldOverride }); 69 66 } 70 67 });
+7 -3
source/popup.tsx
··· 5 5 import browserAPI from 'browser'; 6 6 7 7 import Autoselector from './utilities/autoselector.ts'; 8 - import FaviconData from './utilities/favicon_data.ts'; 8 + import { 9 + createFaviconDataFromEmoji, 10 + getEmojiFromFavicon, 11 + } from './utilities/favicon_data.ts'; 9 12 import useBrowserStorage from './hooks/use_browser_storage.ts'; 10 13 import useStatus from './hooks/use_status.ts'; 11 14 import { ··· 32 35 33 36 const [autoselectedEmoji, autoselectedURL] = useMemo(() => { 34 37 if (!autoselector) return []; 35 - const emoji = autoselector.selectFavicon(url).emoji; 38 + const favicon = autoselector.selectFavicon(url); 39 + const emoji = getEmojiFromFavicon(favicon); 36 40 const faviconURL = createFaviconURLFromChar(emoji?.emoji || ''); 37 41 return [emoji, faviconURL]; 38 42 }, [autoselector, url]); ··· 58 62 .filter(({ matcher }) => matcher !== origin); // Remove dupes 59 63 60 64 if (shouldOverride && autoselectedEmoji) { 61 - siteList.push(new FaviconData(autoselectedEmoji, origin)); 65 + siteList.push(createFaviconDataFromEmoji(origin, autoselectedEmoji)); 62 66 } 63 67 64 68 setCache({ siteList }, true);
+21 -26
source/utilities/__tests__/autoselector.test.ts
··· 1 1 import { assertEquals } from 'asserts'; 2 2 import { it } from 'bdd'; 3 3 4 + import { 5 + createFaviconDataFromEmoji, 6 + getEmojiFromFavicon, 7 + } from '../favicon_data.ts'; 4 8 import Autoselector, { AUTOSELECTOR_VERSION } from '../autoselector.ts'; 5 9 6 10 const { FAVIOLI_LEGACY, UNICODE_12, UNICODE_11, UNICODE_09 } = ··· 9 13 it('Should select emoji', () => { 10 14 const autoselector = new Autoselector(AUTOSELECTOR_VERSION.UNICODE_12); 11 15 const emoji = autoselector.selectFavicon('https://favioli.com'); 12 - assertEquals(emoji, { 13 - emoji: { 14 - aliases: [ 15 - 'tractor', 16 - ], 17 - description: 'tractor', 18 - emoji: '🚜', 19 - emojiVersion: 1, 20 - group: 'Travel & Places', 21 - subgroup: 'transport-ground', 22 - tags: [ 23 - 'tractor', 24 - 'vehicle', 25 - ], 26 - unicodeVersion: 8, 27 - }, 28 - id: 'tractor', 29 - matcher: 'https://favioli.com', 30 - }); 16 + assertEquals( 17 + emoji, 18 + createFaviconDataFromEmoji( 19 + 'https://favioli.com', 20 + getEmojiFromFavicon(emoji), 21 + ), 22 + ); 31 23 }); 32 24 33 25 it('Should select different emojis for different sets', () => { ··· 40 32 const unicode12Emoji = new Autoselector(UNICODE_12) 41 33 .selectFavicon('https://favioli.com'); 42 34 43 - assertEquals(legacyEmoji.emoji?.emoji, '😥'); 44 - assertEquals(unicode12Emoji.emoji?.emoji, '🚜'); 45 - assertEquals(unicode11Emoji.emoji?.emoji, '👄'); 35 + assertEquals(getEmojiFromFavicon(legacyEmoji)?.emoji, '😥'); 36 + assertEquals(getEmojiFromFavicon(unicode12Emoji)?.emoji, '🚜'); 37 + assertEquals(getEmojiFromFavicon(unicode11Emoji)?.emoji, '👄'); 46 38 }); 47 39 48 40 it('Should default to no flags', () => { ··· 52 44 const withNoFlags = new Autoselector(UNICODE_09) 53 45 .selectFavicon('http://bpev.me'); 54 46 55 - assertEquals(includingFlags.emoji?.emoji, '🇬🇲'); 56 - assertEquals(withNoFlags.emoji?.emoji, '🦆'); 47 + assertEquals(getEmojiFromFavicon(includingFlags)?.emoji, '🇬🇲'); 48 + assertEquals(getEmojiFromFavicon(withNoFlags)?.emoji, '🦆'); 57 49 }); 58 50 59 51 it('Should give the same emoji for the same domain', () => { 60 52 const autoselector = new Autoselector(UNICODE_12); 61 53 assertEquals( 62 - autoselector.selectFavicon('https://favioli.com').emoji, 63 - autoselector.selectFavicon('http://favioli.com/lala/blah?hehe=hoho').emoji, 54 + getEmojiFromFavicon(autoselector.selectFavicon('https://favioli.com')) 55 + ?.emoji, 56 + getEmojiFromFavicon( 57 + autoselector.selectFavicon('http://favioli.com/lala/blah?hehe=hoho'), 58 + )?.emoji, 64 59 ); 65 60 });
+5 -5
source/utilities/__tests__/fixtures/settings_data.ts
··· 1 1 import type { Settings, SettingsV1 } from '../../settings.ts'; 2 2 3 3 import * as emoji from 'emoji'; 4 - import FaviconData from '../../favicon_data.ts'; 4 + import { createFaviconDataFromEmoji } from '../../favicon_data.ts'; 5 5 6 6 export const v0: SettingsV1 = { 7 7 'flagReplaced': true, ··· 97 97 'enableSiteIgnore': true, 98 98 }, 99 99 'ignoreList': [ 100 - new FaviconData(undefined, 'hahahahh'), 100 + createFaviconDataFromEmoji('hahahahh'), 101 101 ], 102 102 'siteList': [ 103 - new FaviconData(emoji.infoByCode('😍'), 'hello'), 104 - new FaviconData(emoji.infoByCode('😃'), 'goodbye'), 105 - new FaviconData(emoji.infoByCode('🤩'), 'sweet lahd'), 103 + createFaviconDataFromEmoji('hello', emoji.infoByCode('😍')), 104 + createFaviconDataFromEmoji('goodbye', emoji.infoByCode('😃')), 105 + createFaviconDataFromEmoji('sweet lahd', emoji.infoByCode('🤩')), 106 106 ], 107 107 'version': '2.0.0', 108 108 };
+6 -7
source/utilities/autoselector.ts
··· 1 1 import * as emoji from 'emoji'; 2 2 import LEGACY_EMOJI_SET from '../config/legacy_autoselect_set.ts'; 3 - import FaviconData from '../utilities/favicon_data.ts'; 3 + import { 4 + createFaviconDataFromEmoji, 5 + FaviconData, 6 + } from '../utilities/favicon_data.ts'; 4 7 import { Emoji } from './emoji.ts'; 5 8 6 9 export const AUTOSELECTOR_VERSION = Object.freeze({ ··· 76 79 } 77 80 78 81 const index = Math.abs(sdbm(hostname || url)) % this.favicons.length; 79 - const favicon = this.favicons[index] || this.favicons[0]; 80 - return { 81 - id: favicon?.description || `autoselected-${index}`, 82 - matcher: url, 83 - emoji: favicon, // Always emoji; custom emojis are not in autoset 84 - }; 82 + const emoji = this.favicons[index] || this.favicons[0]; 83 + return createFaviconDataFromEmoji(url, emoji); 85 84 } 86 85 } 87 86
+68 -16
source/utilities/favicon_data.ts
··· 1 1 import * as emoji from 'emoji'; 2 - import type { Emoji } from 'https://deno.land/x/emoji@0.2.0/types.ts'; 2 + import type { Emoji } from './emoji.ts'; 3 + 4 + export interface EmojiMap { 5 + [name: string]: Emoji; 6 + } 7 + 8 + export interface EmojiGroup { 9 + name: string; 10 + representativeEmoji: string; 11 + emojis: Emoji[]; 12 + } 3 13 4 - const DEFAULT_EMOJI = emoji.infoByCode('😀') as Emoji; 14 + export interface EmojiGroups { 15 + [name: string]: EmojiGroup; 16 + } 17 + 18 + export const emojis = emoji.all(); 19 + export const byDescription: EmojiMap = Object.fromEntries( 20 + emojis.map((emoji) => { 21 + return [emoji.description, emoji]; 22 + }), 23 + ); 24 + 25 + export const emojiGroups: EmojiGroups = {}; 26 + emojis.forEach((emoji) => { 27 + if (!emojiGroups[emoji.group]) { 28 + emojiGroups[emoji.group] = { 29 + name: emoji.group, 30 + emojis: [emoji], 31 + representativeEmoji: emoji.emoji, 32 + }; 33 + } else { 34 + emojiGroups[emoji.group].emojis.push(emoji); 35 + } 36 + }); 37 + 38 + emojiGroups['Custom Emojis'] = { 39 + name: 'Custom Emojis', 40 + emojis: [], 41 + representativeEmoji: '*', 42 + }; 43 + 44 + export const emojiGroupsArray = Object.keys(emojiGroups).map((name) => 45 + emojiGroups[name] 46 + ); 47 + 48 + export const DEFAULT_EMOJI = Object.freeze(emoji.infoByCode('😀') as Emoji); 49 + 50 + export function getEmojiFromFavicon( 51 + favicon?: FaviconData, 52 + options?: { customEmojis?: EmojiMap }, 53 + ): Emoji | undefined { 54 + if (!favicon) return; 55 + const customEmoji = options?.customEmojis?.[favicon.id]; 56 + return customEmoji || byDescription[favicon.id]; 57 + } 58 + 59 + export function createFaviconDataFromEmoji( 60 + matcher = '', 61 + emojiInput?: Emoji, 62 + ) { 63 + const id = (emojiInput || DEFAULT_EMOJI).description; 64 + return { id, matcher }; 65 + } 5 66 6 67 /** 7 - * Local Icon Database 8 - * Used to store favicons for later usage. 68 + * Store by ID, and retrieve from storage when we want to use. 69 + * This allows us to save custom emoji image data and access on demand, 70 + * saving us space in chrome storage. 9 71 */ 10 - export default class FaviconData { 11 - id: string; // Unique ID representing favicon (nickname, etc) 72 + export interface FaviconData { 73 + id: string; // Unique ID representing favicon (emoji.description) 12 74 matcher: string; // String (inc RegExp string) representing the url to match 13 - video?: string; // Priority 1: Optional multiframe favicon 14 - image?: string; // Priority 2: Optional singleframe favicon 15 - emoji?: Emoji; // Priority 3: Optional emoji favicon 16 - 17 - constructor(emojiInput?: Emoji, matcher: string = '') { 18 - const selectedEmoji = emojiInput || DEFAULT_EMOJI; 19 - this.id = selectedEmoji.description; 20 - this.matcher = matcher; 21 - this.emoji = selectedEmoji; 22 - } 23 75 }
+3 -3
source/utilities/settings.ts
··· 1 1 import * as emoji from 'emoji'; 2 2 import manifest from '../manifest.json' assert { type: 'json' }; 3 - import FaviconData from './favicon_data.ts'; 3 + import { createFaviconDataFromEmoji, FaviconData } from './favicon_data.ts'; 4 4 import { Emoji } from './emoji.ts'; 5 5 6 6 import { AUTOSELECTOR_VERSION } from './autoselector.ts'; ··· 133 133 const emojiInput = typeof legacyFavicon.emoji === 'string' 134 134 ? emoji.infoByCode(legacyFavicon.emoji) 135 135 : emoji.infoByCode(legacyFavicon.emoji.native); 136 - return new FaviconData(emojiInput, legacyFavicon.filter); 136 + return createFaviconDataFromEmoji(legacyFavicon.filter, emojiInput); 137 137 }); 138 138 139 139 settings.ignoreList = (legacySettings?.skips || []) 140 - .map((skip) => new FaviconData(undefined, skip)); 140 + .map((skip) => createFaviconDataFromEmoji(skip)); 141 141 142 142 settings.autoselectorVersion = AUTOSELECTOR_VERSION.FAVIOLI_LEGACY; 143 143