MuteSky provides a specialized keyword muting system that respects user's existing muted keywords while providing a curated list of additional keywords to mute. The system is non-destructive - it never removes keywords that users have muted outside of our curated list.
class MuteService {
constructor(session) {
this.agent = session ? new Agent(session) : null;
this.session = session;
this.cachedKeywords = null;
this.cachedPreferences = null;
}
}
const settings = loadMuteSettings();
{
scope: 'all' | 'tags-only', // Where to apply muting
duration: number, // Mute duration in days
excludeFollows: boolean // Whether to exclude followed users
}
-
Curated Keywords
- From our predefined list
- Can be muted/unmuted through UI
- Case-insensitive matching
- Original case preserved when muting
- Support mute settings (scope, duration, excludes)
-
User's Custom Keywords
- Muted outside our list
- Never shown in UI but tracked
- Preserved during all operations
- Stored lowercase for comparison
- Original settings preserved
// Separate user's custom keywords
const userCustomKeywords = currentMutedPref.items
.filter(item => !ourKeywordsSet.has(item.value.toLowerCase()))
.map(item => ({
value: item.value,
targets: item.targets || ['content', 'tag']
}));
// Create new items for selected keywords
const newManagedItems = selectedKeywords
.filter(keyword => ourKeywordsSet.has(keyword.toLowerCase()))
.map(keyword => ({
value: keyword,
targets: settings.scope === 'tags-only' ? ['tag'] : ['content', 'tag'],
...(settings.excludeFollows && { actorTarget: 'notFollowed' }),
...(expiresAt && { expires: expiresAt.toISOString() })
}));
// Combine preserving user's keywords
const updatedItems = [
...userCustomKeywords, // Preserve all user's custom keywords
...newManagedItems // Only include selected keywords from our list
];
// Store all keywords in lowercase for comparison
userKeywords.forEach(keyword => {
const lowerKeyword = keyword.toLowerCase();
state.originalMutedKeywords.add(lowerKeyword);
// If it's one of our managed keywords, add to activeKeywords with proper case
const originalCase = ourKeywordsMap.get(lowerKeyword);
if (originalCase) {
state.activeKeywords.add(originalCase);
cache.invalidateCategory(getKeywordCategory(originalCase));
}
});
class MuteService {
async getMutedKeywords(forceRefresh = false) {
// Return cached keywords if available
if (!forceRefresh && this.cachedKeywords !== null) {
return this.cachedKeywords;
}
// Fetch and cache new keywords
const response = await agent.api.app.bsky.actor.getPreferences();
this.cachedPreferences = response.data.preferences;
const mutedWordsPref = this.cachedPreferences.find(
pref => pref.$type === 'app.bsky.actor.defs#mutedWordsPref'
);
this.cachedKeywords = mutedWordsPref?.items?.map(item => item.value) || [];
return this.cachedKeywords;
}
}
async function initializeKeywordState() {
// Get user's currently muted keywords
const mutedKeywords = await muteService.getMutedKeywords();
// Store in lowercase for comparison
state.originalMutedKeywords = new Set(
mutedKeywords.map(k => k.toLowerCase())
);
// Show checkmarks for our keywords that user has muted
const ourKeywordsMap = getOurKeywordsMap();
mutedKeywords.forEach(keyword => {
const originalCase = ourKeywordsMap.get(keyword.toLowerCase());
if (originalCase) {
state.activeKeywords.add(originalCase);
}
});
}
async function handleMuteSubmit() {
// Get current mute settings
const settings = loadMuteSettings();
// Apply settings to keywords
const keywordsToMute = Array.from(state.activeKeywords)
.filter(k => !state.originalMutedKeywords.has(k.toLowerCase()));
// Update on Bluesky
await muteService.updateMutedKeywords(keywordsToMute, ourKeywordsList);
// Update local state
keywordsToMute.forEach(k => {
state.originalMutedKeywords.add(k.toLowerCase());
state.sessionMutedKeywords.add(k);
});
}
// Can only unmute our keywords
function canUnmuteKeyword(keyword) {
return ourKeywordsMap.has(keyword.toLowerCase());
}
// Preserve user's custom keywords during unmute
const userCustomKeywords = currentMutedPref.items
.filter(item => !ourKeywordsSet.has(item.value.toLowerCase()));
if (error.status === 401) {
// Dispatch event for session refresh
const refreshEvent = new CustomEvent('mutesky:session:refresh:needed');
window.dispatchEvent(refreshEvent);
// Clear caches
this.cachedKeywords = null;
this.cachedPreferences = null;
}
setSession(session) {
this.agent = session ? new Agent(session) : null;
this.session = session;
// Clear caches when session changes
this.cachedKeywords = null;
this.cachedPreferences = null;
}
-
Keyword Management
- Store keywords lowercase for comparison
- Preserve original case for display/muting
- Never modify user's custom keywords
- Track both custom and managed keywords
- Use caching for efficient lookups
-
Error Prevention
- Validate session before operations
- Handle case sensitivity properly
- Verify keyword existence
- Maintain consistent state
- Clear caches on errors
-
Performance
- Cache expensive API calls
- Batch keyword updates
- Throttle rapid operations
- Clear caches appropriately
- Use efficient data structures
-
User Experience
- Preserve user preferences
- Provide clear feedback
- Handle errors gracefully
- Maintain responsive UI
- Show accurate mute counts