Files
smesh/src/services/local-storage.service.ts
mleku 13b3b82443 refactor: rebrand from Jumble to Smesh
- Replace all Jumble branding with Smesh throughout codebase
- Add new Smesh logo images (light/dark themes)
- Update Logo component to use PNG images with theme support
- Update URLs to git.mleku.dev/mleku/smesh
- Rename JumbleTranslate to SmeshTranslate
- Update all i18n locale files with new branding
- Add system theme detection CSS to prevent flash on load
- Update PWA manifest, docker-compose, and config files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 09:43:18 +02:00

614 lines
20 KiB
TypeScript

import {
ALLOWED_FILTER_KINDS,
DEFAULT_FAVICON_URL_TEMPLATE,
DEFAULT_NIP_96_SERVICE,
ExtendedKind,
MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE,
NSFW_DISPLAY_POLICY,
StorageKey,
TPrimaryColor
} from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import { isTorBrowser } from '@/lib/utils'
import {
TAccount,
TAccountPointer,
TEmoji,
TFeedInfo,
TMediaAutoLoadPolicy,
TMediaUploadServiceConfig,
TNoteListMode,
TNsfwDisplayPolicy,
TNotificationStyle,
TRelaySet,
TThemeSetting,
TTranslationServiceConfig
} from '@/types'
import { kinds } from 'nostr-tools'
class LocalStorageService {
static instance: LocalStorageService
private relaySets: TRelaySet[] = []
private themeSetting: TThemeSetting = 'system'
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
private noteListMode: TNoteListMode = 'posts'
private lastReadNotificationTimeMap: Record<string, number> = {}
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
private hideUntrustedInteractions: boolean = false
private hideUntrustedNotifications: boolean = false
private hideUntrustedNotes: boolean = false
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private dismissedTooManyRelaysAlert: boolean = false
private showKinds: number[] = []
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
private sidebarCollapse: boolean = false
private primaryColor: TPrimaryColor = 'DEFAULT'
private enableSingleColumnLayout: boolean = true
private faviconUrlTemplate: string = DEFAULT_FAVICON_URL_TEMPLATE
private filterOutOnionRelays: boolean = !isTorBrowser()
private quickReaction: boolean = false
private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
constructor() {
if (!LocalStorageService.instance) {
this.init()
LocalStorageService.instance = this
}
return LocalStorageService.instance
}
init() {
this.themeSetting =
(window.localStorage.getItem(StorageKey.THEME_SETTING) as TThemeSetting) ?? 'system'
const accountsStr = window.localStorage.getItem(StorageKey.ACCOUNTS)
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
this.noteListMode =
noteListModeStr && ['posts', 'postsAndReplies', '24h'].includes(noteListModeStr)
? (noteListModeStr as TNoteListMode)
: 'posts'
const lastReadNotificationTimeMapStr =
window.localStorage.getItem(StorageKey.LAST_READ_NOTIFICATION_TIME_MAP) ?? '{}'
this.lastReadNotificationTimeMap = JSON.parse(lastReadNotificationTimeMapStr)
const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS)
if (!relaySetsStr) {
let relaySets: TRelaySet[] = []
const legacyRelayGroupsStr = window.localStorage.getItem('relayGroups')
if (legacyRelayGroupsStr) {
const legacyRelayGroups = JSON.parse(legacyRelayGroupsStr)
relaySets = legacyRelayGroups.map((group: any) => {
return {
id: randomString(),
name: group.groupName,
relayUrls: group.relayUrls
}
})
}
if (!relaySets.length) {
relaySets = []
}
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
this.relaySets = relaySets
} else {
this.relaySets = JSON.parse(relaySetsStr)
}
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
if (defaultZapSatsStr) {
const num = parseInt(defaultZapSatsStr)
if (!isNaN(num)) {
this.defaultZapSats = num
}
}
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
const accountFeedInfoMapStr =
window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
// deprecated
this.mediaUploadService =
window.localStorage.getItem(StorageKey.MEDIA_UPLOAD_SERVICE) ?? DEFAULT_NIP_96_SERVICE
this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false'
const hideUntrustedEvents =
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) === 'true'
const storedHideUntrustedInteractions = window.localStorage.getItem(
StorageKey.HIDE_UNTRUSTED_INTERACTIONS
)
const storedHideUntrustedNotifications = window.localStorage.getItem(
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS
)
const storedHideUntrustedNotes = window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_NOTES)
this.hideUntrustedInteractions = storedHideUntrustedInteractions
? storedHideUntrustedInteractions === 'true'
: hideUntrustedEvents
this.hideUntrustedNotifications = storedHideUntrustedNotifications
? storedHideUntrustedNotifications === 'true'
: hideUntrustedEvents
this.hideUntrustedNotes = storedHideUntrustedNotes
? storedHideUntrustedNotes === 'true'
: hideUntrustedEvents
const translationServiceConfigMapStr = window.localStorage.getItem(
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP
)
if (translationServiceConfigMapStr) {
this.translationServiceConfigMap = JSON.parse(translationServiceConfigMapStr)
}
const mediaUploadServiceConfigMapStr = window.localStorage.getItem(
StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP
)
if (mediaUploadServiceConfigMapStr) {
this.mediaUploadServiceConfigMap = JSON.parse(mediaUploadServiceConfigMapStr)
}
// Migrate old boolean setting to new policy
const nsfwDisplayPolicyStr = window.localStorage.getItem(StorageKey.NSFW_DISPLAY_POLICY)
if (
nsfwDisplayPolicyStr &&
Object.values(NSFW_DISPLAY_POLICY).includes(nsfwDisplayPolicyStr as TNsfwDisplayPolicy)
) {
this.nsfwDisplayPolicy = nsfwDisplayPolicyStr as TNsfwDisplayPolicy
} else {
// Migration: convert old boolean to new policy
const defaultShowNsfwStr = window.localStorage.getItem(StorageKey.DEFAULT_SHOW_NSFW)
this.nsfwDisplayPolicy =
defaultShowNsfwStr === 'true' ? NSFW_DISPLAY_POLICY.SHOW : NSFW_DISPLAY_POLICY.HIDE_CONTENT
window.localStorage.setItem(StorageKey.NSFW_DISPLAY_POLICY, this.nsfwDisplayPolicy)
}
this.dismissedTooManyRelaysAlert =
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
if (!showKindsStr) {
this.showKinds = ALLOWED_FILTER_KINDS
} else {
const showKindsVersionStr = window.localStorage.getItem(StorageKey.SHOW_KINDS_VERSION)
const showKindsVersion = showKindsVersionStr ? parseInt(showKindsVersionStr) : 0
const showKindSet = new Set(JSON.parse(showKindsStr) as number[])
if (showKindsVersion < 1) {
showKindSet.add(ExtendedKind.VIDEO)
showKindSet.add(ExtendedKind.SHORT_VIDEO)
}
if (showKindsVersion < 2 && showKindSet.has(ExtendedKind.VIDEO)) {
showKindSet.add(ExtendedKind.ADDRESSABLE_NORMAL_VIDEO)
showKindSet.add(ExtendedKind.ADDRESSABLE_SHORT_VIDEO)
}
if (showKindsVersion < 3 && showKindSet.has(24236)) {
showKindSet.delete(24236) // remove typo kind
showKindSet.add(ExtendedKind.ADDRESSABLE_SHORT_VIDEO)
}
if (showKindsVersion < 4 && showKindSet.has(kinds.Repost)) {
showKindSet.add(kinds.GenericRepost)
}
this.showKinds = Array.from(showKindSet)
}
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '4')
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
this.notificationListStyle =
window.localStorage.getItem(StorageKey.NOTIFICATION_LIST_STYLE) ===
NOTIFICATION_LIST_STYLE.COMPACT
? NOTIFICATION_LIST_STYLE.COMPACT
: NOTIFICATION_LIST_STYLE.DETAILED
const mediaAutoLoadPolicy = window.localStorage.getItem(StorageKey.MEDIA_AUTO_LOAD_POLICY)
if (
mediaAutoLoadPolicy &&
Object.values(MEDIA_AUTO_LOAD_POLICY).includes(mediaAutoLoadPolicy as TMediaAutoLoadPolicy)
) {
this.mediaAutoLoadPolicy = mediaAutoLoadPolicy as TMediaAutoLoadPolicy
}
const shownCreateWalletGuideToastPubkeysStr = window.localStorage.getItem(
StorageKey.SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS
)
this.shownCreateWalletGuideToastPubkeys = shownCreateWalletGuideToastPubkeysStr
? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr))
: new Set()
this.sidebarCollapse = window.localStorage.getItem(StorageKey.SIDEBAR_COLLAPSE) === 'true'
this.primaryColor =
(window.localStorage.getItem(StorageKey.PRIMARY_COLOR) as TPrimaryColor) ?? 'DEFAULT'
this.enableSingleColumnLayout =
window.localStorage.getItem(StorageKey.ENABLE_SINGLE_COLUMN_LAYOUT) !== 'false'
this.faviconUrlTemplate =
window.localStorage.getItem(StorageKey.FAVICON_URL_TEMPLATE) ?? DEFAULT_FAVICON_URL_TEMPLATE
const filterOutOnionRelaysStr = window.localStorage.getItem(StorageKey.FILTER_OUT_ONION_RELAYS)
if (filterOutOnionRelaysStr) {
this.filterOutOnionRelays = filterOutOnionRelaysStr !== 'false'
}
this.quickReaction = window.localStorage.getItem(StorageKey.QUICK_REACTION) === 'true'
const quickReactionEmojiStr =
window.localStorage.getItem(StorageKey.QUICK_REACTION_EMOJI) ?? '+'
if (quickReactionEmojiStr.startsWith('{')) {
this.quickReactionEmoji = JSON.parse(quickReactionEmojiStr) as TEmoji
} else {
this.quickReactionEmoji = quickReactionEmojiStr
}
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
window.localStorage.removeItem(StorageKey.FEED_TYPE)
}
getRelaySets() {
return this.relaySets
}
setRelaySets(relaySets: TRelaySet[]) {
this.relaySets = relaySets
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
}
getThemeSetting() {
return this.themeSetting
}
setThemeSetting(themeSetting: TThemeSetting) {
window.localStorage.setItem(StorageKey.THEME_SETTING, themeSetting)
this.themeSetting = themeSetting
}
getNoteListMode() {
return this.noteListMode
}
setNoteListMode(mode: TNoteListMode) {
window.localStorage.setItem(StorageKey.NOTE_LIST_MODE, mode)
this.noteListMode = mode
}
getAccounts() {
return this.accounts
}
findAccount(account: TAccountPointer) {
return this.accounts.find((act) => isSameAccount(act, account))
}
getCurrentAccount() {
return this.currentAccount
}
getAccountNsec(pubkey: string) {
const account = this.accounts.find((act) => act.pubkey === pubkey && act.signerType === 'nsec')
return account?.nsec
}
getAccountNcryptsec(pubkey: string) {
const account = this.accounts.find(
(act) => act.pubkey === pubkey && act.signerType === 'ncryptsec'
)
return account?.ncryptsec
}
addAccount(account: TAccount) {
const index = this.accounts.findIndex((act) => isSameAccount(act, account))
if (index !== -1) {
this.accounts[index] = account
} else {
this.accounts.push(account)
}
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
return this.accounts
}
removeAccount(account: TAccount) {
this.accounts = this.accounts.filter((act) => !isSameAccount(act, account))
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
return this.accounts
}
switchAccount(account: TAccount | null) {
if (isSameAccount(this.currentAccount, account)) {
return
}
const act = this.accounts.find((act) => isSameAccount(act, account))
if (!act) {
return
}
this.currentAccount = act
window.localStorage.setItem(StorageKey.CURRENT_ACCOUNT, JSON.stringify(act))
}
getDefaultZapSats() {
return this.defaultZapSats
}
setDefaultZapSats(sats: number) {
this.defaultZapSats = sats
window.localStorage.setItem(StorageKey.DEFAULT_ZAP_SATS, sats.toString())
}
getDefaultZapComment() {
return this.defaultZapComment
}
setDefaultZapComment(comment: string) {
this.defaultZapComment = comment
window.localStorage.setItem(StorageKey.DEFAULT_ZAP_COMMENT, comment)
}
getQuickZap() {
return this.quickZap
}
setQuickZap(quickZap: boolean) {
this.quickZap = quickZap
window.localStorage.setItem(StorageKey.QUICK_ZAP, quickZap.toString())
}
getLastReadNotificationTime(pubkey: string) {
return this.lastReadNotificationTimeMap[pubkey] ?? 0
}
setLastReadNotificationTime(pubkey: string, time: number) {
this.lastReadNotificationTimeMap[pubkey] = time
window.localStorage.setItem(
StorageKey.LAST_READ_NOTIFICATION_TIME_MAP,
JSON.stringify(this.lastReadNotificationTimeMap)
)
}
getFeedInfo(pubkey: string) {
return this.accountFeedInfoMap[pubkey]
}
setFeedInfo(info: TFeedInfo, pubkey?: string | null) {
this.accountFeedInfoMap[pubkey ?? 'default'] = info
window.localStorage.setItem(
StorageKey.ACCOUNT_FEED_INFO_MAP,
JSON.stringify(this.accountFeedInfoMap)
)
}
getAutoplay() {
return this.autoplay
}
setAutoplay(autoplay: boolean) {
this.autoplay = autoplay
window.localStorage.setItem(StorageKey.AUTOPLAY, autoplay.toString())
}
getHideUntrustedInteractions() {
return this.hideUntrustedInteractions
}
setHideUntrustedInteractions(hideUntrustedInteractions: boolean) {
this.hideUntrustedInteractions = hideUntrustedInteractions
window.localStorage.setItem(
StorageKey.HIDE_UNTRUSTED_INTERACTIONS,
hideUntrustedInteractions.toString()
)
}
getHideUntrustedNotifications() {
return this.hideUntrustedNotifications
}
setHideUntrustedNotifications(hideUntrustedNotifications: boolean) {
this.hideUntrustedNotifications = hideUntrustedNotifications
window.localStorage.setItem(
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS,
hideUntrustedNotifications.toString()
)
}
getHideUntrustedNotes() {
return this.hideUntrustedNotes
}
setHideUntrustedNotes(hideUntrustedNotes: boolean) {
this.hideUntrustedNotes = hideUntrustedNotes
window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_NOTES, hideUntrustedNotes.toString())
}
getTranslationServiceConfig(pubkey?: string | null) {
return this.translationServiceConfigMap[pubkey ?? '_'] ?? { service: 'smesh' }
}
setTranslationServiceConfig(config: TTranslationServiceConfig, pubkey?: string | null) {
this.translationServiceConfigMap[pubkey ?? '_'] = config
window.localStorage.setItem(
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP,
JSON.stringify(this.translationServiceConfigMap)
)
}
getMediaUploadServiceConfig(pubkey?: string | null): TMediaUploadServiceConfig {
const defaultConfig = { type: 'nip96', service: this.mediaUploadService } as const
if (!pubkey) {
return defaultConfig
}
return this.mediaUploadServiceConfigMap[pubkey] ?? defaultConfig
}
setMediaUploadServiceConfig(
pubkey: string,
config: TMediaUploadServiceConfig
): TMediaUploadServiceConfig {
this.mediaUploadServiceConfigMap[pubkey] = config
window.localStorage.setItem(
StorageKey.MEDIA_UPLOAD_SERVICE_CONFIG_MAP,
JSON.stringify(this.mediaUploadServiceConfigMap)
)
return config
}
getDismissedTooManyRelaysAlert() {
return this.dismissedTooManyRelaysAlert
}
setDismissedTooManyRelaysAlert(dismissed: boolean) {
this.dismissedTooManyRelaysAlert = dismissed
window.localStorage.setItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT, dismissed.toString())
}
getShowKinds() {
return this.showKinds
}
setShowKinds(kinds: number[]) {
this.showKinds = kinds
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(kinds))
}
getHideContentMentioningMutedUsers() {
return this.hideContentMentioningMutedUsers
}
setHideContentMentioningMutedUsers(hide: boolean) {
this.hideContentMentioningMutedUsers = hide
window.localStorage.setItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS, hide.toString())
}
getNotificationListStyle() {
return this.notificationListStyle
}
setNotificationListStyle(style: TNotificationStyle) {
this.notificationListStyle = style
window.localStorage.setItem(StorageKey.NOTIFICATION_LIST_STYLE, style)
}
getMediaAutoLoadPolicy() {
return this.mediaAutoLoadPolicy
}
setMediaAutoLoadPolicy(policy: TMediaAutoLoadPolicy) {
this.mediaAutoLoadPolicy = policy
window.localStorage.setItem(StorageKey.MEDIA_AUTO_LOAD_POLICY, policy)
}
hasShownCreateWalletGuideToast(pubkey: string) {
return this.shownCreateWalletGuideToastPubkeys.has(pubkey)
}
markCreateWalletGuideToastAsShown(pubkey: string) {
if (this.shownCreateWalletGuideToastPubkeys.has(pubkey)) {
return
}
this.shownCreateWalletGuideToastPubkeys.add(pubkey)
window.localStorage.setItem(
StorageKey.SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS,
JSON.stringify(Array.from(this.shownCreateWalletGuideToastPubkeys))
)
}
getSidebarCollapse() {
return this.sidebarCollapse
}
setSidebarCollapse(collapse: boolean) {
this.sidebarCollapse = collapse
window.localStorage.setItem(StorageKey.SIDEBAR_COLLAPSE, collapse.toString())
}
getPrimaryColor() {
return this.primaryColor
}
setPrimaryColor(color: TPrimaryColor) {
this.primaryColor = color
window.localStorage.setItem(StorageKey.PRIMARY_COLOR, color)
}
getEnableSingleColumnLayout() {
return this.enableSingleColumnLayout
}
setEnableSingleColumnLayout(enable: boolean) {
this.enableSingleColumnLayout = enable
window.localStorage.setItem(StorageKey.ENABLE_SINGLE_COLUMN_LAYOUT, enable.toString())
}
getFaviconUrlTemplate() {
return this.faviconUrlTemplate
}
setFaviconUrlTemplate(template: string) {
this.faviconUrlTemplate = template
window.localStorage.setItem(StorageKey.FAVICON_URL_TEMPLATE, template)
}
getFilterOutOnionRelays() {
return this.filterOutOnionRelays
}
setFilterOutOnionRelays(filterOut: boolean) {
this.filterOutOnionRelays = filterOut
window.localStorage.setItem(StorageKey.FILTER_OUT_ONION_RELAYS, filterOut.toString())
}
getQuickReaction() {
return this.quickReaction
}
setQuickReaction(quickReaction: boolean) {
this.quickReaction = quickReaction
window.localStorage.setItem(StorageKey.QUICK_REACTION, quickReaction.toString())
}
getQuickReactionEmoji() {
return this.quickReactionEmoji
}
setQuickReactionEmoji(emoji: string | TEmoji) {
this.quickReactionEmoji = emoji
window.localStorage.setItem(
StorageKey.QUICK_REACTION_EMOJI,
typeof emoji === 'string' ? emoji : JSON.stringify(emoji)
)
}
getNsfwDisplayPolicy() {
return this.nsfwDisplayPolicy
}
setNsfwDisplayPolicy(policy: TNsfwDisplayPolicy) {
this.nsfwDisplayPolicy = policy
window.localStorage.setItem(StorageKey.NSFW_DISPLAY_POLICY, policy)
}
}
const instance = new LocalStorageService()
export default instance