Enhance compose experience with comprehensive autocomplete systems

## Major Features Added

### Smart Mention Completion
- **Full fediverse handles**: @user@instance.com format instead of incomplete usernames
- **Multi-source suggestions**: Search API + followers + following for comprehensive results
- **Real-time API integration**: No more hardcoded sample data
- **Intelligent filtering**: Prefix-based matching across all user connections

### Comprehensive Emoji System
- **5,000+ Unicode emojis**: Complete emoji dataset via python-emoji library
- **Keyword-based search**: Find emojis by typing descriptive words (:fire, :heart, :grin)
- **Actual emoji insertion**: Inserts Unicode characters (🎃) not shortcodes (🎃)
- **Accurate selection**: Fixed bug where wrong emoji was inserted from autocomplete list
- **Smart synonyms**: Common aliases for frequently used emojis

### Enhanced ActivityPub Integration
- **Account relationships**: get_followers(), get_following(), search_accounts() methods
- **Expanded API coverage**: Better integration with fediverse social graph
- **Robust error handling**: Graceful fallbacks for API failures

### User Experience Improvements
- **Bug fixes**: Resolved autocomplete selection and focus restoration issues
- **Documentation**: Updated README with comprehensive feature descriptions
- **Dependencies**: Added emoji>=2.0.0 to requirements for Unicode support

## Technical Details
- Removed incomplete fallback data in favor of live API integration
- Improved completer selection logic to use actually selected items
- Enhanced error handling for network requests and API limitations
- Updated installation instructions for new emoji library dependency

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Storm Dragon
2025-07-20 16:06:04 -04:00
parent f2c4ad1bd6
commit 6fa0cf481a
5 changed files with 246 additions and 128 deletions

View File

@ -16,23 +16,41 @@ This project was created through "vibe coding" - a collaborative development app
- **Timeline Switching**: Easy navigation between Home, Mentions, Local, and Federated timelines - **Timeline Switching**: Easy navigation between Home, Mentions, Local, and Federated timelines
- **Desktop Notifications**: Cross-platform notifications for mentions, direct messages, and timeline updates - **Desktop Notifications**: Cross-platform notifications for mentions, direct messages, and timeline updates
- **Customizable Audio Feedback**: Rich sound pack system with themed audio notifications - **Customizable Audio Feedback**: Rich sound pack system with themed audio notifications
- **Soundpack Manager**: Secure repository-based soundpack discovery and installation
- **Smart Autocomplete**: Mention completion with full fediverse handles (@user@instance.com)
- **Comprehensive Emoji Support**: 5,000+ Unicode emojis with keyword search
- **Auto-refresh**: Intelligent timeline updates based on user activity
- **Clean Interface**: Focused on functionality over visual design - **Clean Interface**: Focused on functionality over visual design
- **Keyboard Navigation**: Complete keyboard control with intuitive shortcuts - **Keyboard Navigation**: Complete keyboard control with intuitive shortcuts
## Audio System ## Audio System
Bifrost includes a sophisticated sound system with: Bifrost includes a sophisticated sound system with:
- Customizable sound packs (includes Default sounds) - **Soundpack Manager**: Secure HTTPS-based repository system
- Audio feedback for all major actions - **Repository Management**: Add/remove soundpack repositories with validation
- Per-event volume control - **One-click Installation**: Download, validate, and install soundpacks securely
- Cross-platform audio support - **Customizable Sound Packs**: Themed audio notifications (Default pack included)
- **Audio Feedback**: Sound events for all major actions and notifications
- **Per-event Volume Control**: Fine-tune individual sound effects
- **Cross-platform Audio Support**: Works on Linux, Windows, and macOS
## Compose Features
- **Mention Autocomplete**: Type `@` to get suggestions from followers/following/search
- **Full Fediverse Handles**: Completes to full format (@user@instance.com)
- **Emoji Autocomplete**: Type `:` to search 5,000+ Unicode emojis
- **Keyword Search**: Find emojis by typing keywords (`:fire`, `:heart`, `:grin`)
- **Real-time Character Count**: Visual feedback with limit warnings
- **Content Warnings**: Optional spoiler text support
- **Visibility Controls**: Public, Unlisted, Followers-only, or Direct messages
## Technology Stack ## Technology Stack
- **PySide6**: Main GUI framework for proven accessibility - **PySide6**: Main GUI framework for proven accessibility
- **ActivityPub**: Full federation protocol support - **ActivityPub**: Full federation protocol support
- **simpleaudio**: Cross-platform audio with subprocess fallback - **simpleaudio**: Cross-platform audio with subprocess fallback
- **Plyer**: Cross-platform desktop notifications - **Plyer**: Cross-platform desktop notifications
- **emoji**: Comprehensive Unicode emoji library (5,000+ emojis)
- **XDG Base Directory**: Standards-compliant configuration storage - **XDG Base Directory**: Standards-compliant configuration storage
## Keyboard Shortcuts ## Keyboard Shortcuts
@ -60,9 +78,18 @@ Bifrost includes a sophisticated sound system with:
- **Enter**: Expand/collapse threads - **Enter**: Expand/collapse threads
- **Tab**: Move between interface elements - **Tab**: Move between interface elements
### Compose Dialog
- **Ctrl+Enter**: Send post
- **@**: Trigger mention autocomplete
- **:**: Trigger emoji autocomplete
- **Arrow Keys**: Navigate autocomplete suggestions
- **Enter/Tab**: Accept selected completion
- **Escape**: Close autocomplete or cancel compose
### Application ### Application
- **Ctrl+,**: Open Settings - **Ctrl+,**: Open Settings
- **Ctrl+Shift+A**: Add new account - **Ctrl+Shift+A**: Add new account
- **Ctrl+Alt+S**: Open Soundpack Manager
- **Ctrl+Q**: Quit application - **Ctrl+Q**: Quit application
## Installation ## Installation
@ -76,7 +103,7 @@ python bifrost.py
Or on Arch Linux: Or on Arch Linux:
```bash ```bash
sudo pacman -S python-pyside6 python-requests python-simpleaudio sudo pacman -S python-pyside6 python-requests python-simpleaudio python-emoji
yay -S python-plyer yay -S python-plyer
``` ```

View File

@ -1,4 +1,5 @@
PySide6>=6.0.0 PySide6>=6.0.0
requests>=2.25.0 requests>=2.25.0
simpleaudio>=1.0.4 simpleaudio>=1.0.4
plyer>=2.1.0 plyer>=2.1.0
emoji>=2.0.0

View File

@ -178,6 +178,35 @@ class ActivityPubClient:
endpoint = f'/api/v1/accounts/{account_id}/unfollow' endpoint = f'/api/v1/accounts/{account_id}/unfollow'
return self._make_request('POST', endpoint) return self._make_request('POST', endpoint)
def get_followers(self, account_id: str, max_id: Optional[str] = None, limit: int = 40) -> List[Dict]:
"""Get followers for an account"""
params = {'limit': limit}
if max_id:
params['max_id'] = max_id
endpoint = f'/api/v1/accounts/{account_id}/followers'
return self._make_request('GET', endpoint, params=params)
def get_following(self, account_id: str, max_id: Optional[str] = None, limit: int = 40) -> List[Dict]:
"""Get accounts that an account is following"""
params = {'limit': limit}
if max_id:
params['max_id'] = max_id
endpoint = f'/api/v1/accounts/{account_id}/following'
return self._make_request('GET', endpoint, params=params)
def search_accounts(self, query: str, limit: int = 10) -> List[Dict]:
"""Search for accounts by username"""
params = {
'q': query,
'type': 'accounts',
'limit': limit
}
result = self._make_request('GET', '/api/v2/search', params=params)
return result.get('accounts', [])
def search(self, query: str, account_id: Optional[str] = None, def search(self, query: str, account_id: Optional[str] = None,
max_id: Optional[str] = None, min_id: Optional[str] = None, max_id: Optional[str] = None, min_id: Optional[str] = None,
type_filter: Optional[str] = None, limit: int = 20) -> Dict: type_filter: Optional[str] = None, limit: int = 20) -> Dict:

View File

@ -6,6 +6,7 @@ from PySide6.QtWidgets import QTextEdit, QCompleter, QListWidget, QListWidgetIte
from PySide6.QtCore import Qt, Signal, QStringListModel, QRect from PySide6.QtCore import Qt, Signal, QStringListModel, QRect
from PySide6.QtGui import QTextCursor, QKeyEvent from PySide6.QtGui import QTextCursor, QKeyEvent
import re import re
import emoji
from typing import List, Dict from typing import List, Dict
@ -28,113 +29,63 @@ class AutocompleteTextEdit(QTextEdit):
self.completion_start = 0 self.completion_start = 0
self.completion_type = None # 'mention' or 'emoji' self.completion_type = None # 'mention' or 'emoji'
# Load default emojis # Load comprehensive emoji dataset
self.load_default_emojis() self.load_unicode_emojis()
def load_default_emojis(self): def load_unicode_emojis(self):
"""Load a comprehensive set of Unicode emojis""" """Load comprehensive Unicode emoji dataset using emoji library"""
self.emoji_list = [ print("Loading Unicode emoji dataset...")
# Faces self.emoji_list = []
{"shortcode": "smile", "emoji": "😄", "keywords": ["smile", "happy", "joy", "grin"]},
{"shortcode": "laughing", "emoji": "😆", "keywords": ["laugh", "haha", "funny", "lol"]}, # Get all emoji data from the emoji library
{"shortcode": "wink", "emoji": "😉", "keywords": ["wink", "flirt", "hint"]}, emoji_data = emoji.EMOJI_DATA
{"shortcode": "thinking", "emoji": "🤔", "keywords": ["thinking", "hmm", "consider", "ponder"]},
{"shortcode": "shrug", "emoji": "🤷", "keywords": ["shrug", "dunno", "whatever", "idk"]}, for emoji_char, data in emoji_data.items():
{"shortcode": "facepalm", "emoji": "🤦", "keywords": ["facepalm", "disappointed", "doh", "frustrated"]}, # Skip emojis without names
{"shortcode": "crying", "emoji": "😭", "keywords": ["crying", "tears", "sad", "sob"]}, if 'en' not in data:
{"shortcode": "angry", "emoji": "😠", "keywords": ["angry", "mad", "furious", "upset"]}, continue
{"shortcode": "cool", "emoji": "😎", "keywords": ["cool", "sunglasses", "awesome", "rad"]},
{"shortcode": "joy", "emoji": "😂", "keywords": ["joy", "laugh", "tears", "funny"]}, # Extract shortcode from emoji library format (removes colons)
{"shortcode": "heart_eyes", "emoji": "😍", "keywords": ["heart", "eyes", "love", "crush"]}, name = data['en'].strip()
{"shortcode": "kiss", "emoji": "😘", "keywords": ["kiss", "love", "smooch", "mwah"]}, if name.startswith(':') and name.endswith(':'):
{"shortcode": "tired", "emoji": "😴", "keywords": ["tired", "sleep", "sleepy", "zzz"]}, shortcode = name[1:-1] # Remove surrounding colons
{"shortcode": "shocked", "emoji": "😱", "keywords": ["shocked", "surprised", "scared", "omg"]}, else:
shortcode = name.lower().replace(' ', '_').replace('-', '_')
shortcode = re.sub(r'[^a-zA-Z0-9_]', '', shortcode)
# Hearts and symbols if not shortcode:
{"shortcode": "heart", "emoji": "❤️", "keywords": ["heart", "love", "red", "romance"]}, continue
{"shortcode": "blue_heart", "emoji": "💙", "keywords": ["blue", "heart", "love", "cold"]},
{"shortcode": "green_heart", "emoji": "💚", "keywords": ["green", "heart", "love", "nature"]},
{"shortcode": "yellow_heart", "emoji": "💛", "keywords": ["yellow", "heart", "love", "happy"]},
{"shortcode": "purple_heart", "emoji": "💜", "keywords": ["purple", "heart", "love", "royal"]},
{"shortcode": "black_heart", "emoji": "🖤", "keywords": ["black", "heart", "love", "dark"]},
{"shortcode": "broken_heart", "emoji": "💔", "keywords": ["broken", "heart", "sad", "breakup"]},
{"shortcode": "sparkling_heart", "emoji": "💖", "keywords": ["sparkling", "heart", "love", "sparkle"]},
# Gestures # Create keywords from the shortcode
{"shortcode": "thumbsup", "emoji": "👍", "keywords": ["thumbs", "up", "good", "ok", "yes"]}, # Split shortcode by underscores to get individual words
{"shortcode": "thumbsdown", "emoji": "👎", "keywords": ["thumbs", "down", "bad", "no", "dislike"]}, words = shortcode.split('_')
{"shortcode": "wave", "emoji": "👋", "keywords": ["wave", "hello", "hi", "goodbye", "bye"]}, keywords = []
{"shortcode": "clap", "emoji": "👏", "keywords": ["clap", "applause", "bravo", "good"]}, for word in words:
{"shortcode": "pray", "emoji": "🙏", "keywords": ["pray", "thanks", "please", "gratitude"]}, if len(word) >= 2: # Only add words with 2+ chars
{"shortcode": "ok_hand", "emoji": "👌", "keywords": ["ok", "hand", "perfect", "good"]}, keywords.append(word.lower())
{"shortcode": "peace", "emoji": "✌️", "keywords": ["peace", "victory", "two", "fingers"]},
{"shortcode": "crossed_fingers", "emoji": "🤞", "keywords": ["crossed", "fingers", "luck", "hope"]},
# Objects and symbols # Add some common synonyms for frequent emojis
{"shortcode": "fire", "emoji": "🔥", "keywords": ["fire", "hot", "flame", "lit"]}, synonyms = {
{"shortcode": "star", "emoji": "", "keywords": ["star", "favorite", "best", "top"]}, 'grinning_face': ['smile', 'happy', 'grin'],
{"shortcode": "rainbow", "emoji": "🌈", "keywords": ["rainbow", "colorful", "pride", "weather"]}, 'face_with_tears_of_joy': ['laugh', 'lol', 'funny'],
{"shortcode": "lightning", "emoji": "", "keywords": ["lightning", "bolt", "fast", "electric"]}, 'red_heart': ['love', 'heart'],
{"shortcode": "snowflake", "emoji": "❄️", "keywords": ["snowflake", "cold", "winter", "frozen"]}, 'thumbs_up': ['good', 'ok', 'yes', 'like'],
{"shortcode": "sun", "emoji": "☀️", "keywords": ["sun", "sunny", "bright", "weather"]}, 'thumbs_down': ['bad', 'no', 'dislike'],
{"shortcode": "moon", "emoji": "🌙", "keywords": ["moon", "night", "crescent", "sleep"]}, 'fire': ['hot', 'lit', 'flame'],
{"shortcode": "cloud", "emoji": "☁️", "keywords": ["cloud", "weather", "sky", "cloudy"]}, 'star': ['favorite', 'best'],
'waving_hand': ['hello', 'hi', 'bye'],
}
# Food and drinks if shortcode in synonyms:
{"shortcode": "coffee", "emoji": "", "keywords": ["coffee", "drink", "morning", "caffeine"]}, keywords.extend(synonyms[shortcode])
{"shortcode": "tea", "emoji": "🍵", "keywords": ["tea", "drink", "hot", "green"]},
{"shortcode": "beer", "emoji": "🍺", "keywords": ["beer", "drink", "alcohol", "party"]},
{"shortcode": "wine", "emoji": "🍷", "keywords": ["wine", "drink", "alcohol", "red"]},
{"shortcode": "pizza", "emoji": "🍕", "keywords": ["pizza", "food", "italian", "slice"]},
{"shortcode": "burger", "emoji": "🍔", "keywords": ["burger", "food", "meat", "american"]},
{"shortcode": "cake", "emoji": "🎂", "keywords": ["cake", "birthday", "dessert", "sweet"]},
{"shortcode": "cookie", "emoji": "🍪", "keywords": ["cookie", "dessert", "sweet", "snack"]},
{"shortcode": "apple", "emoji": "🍎", "keywords": ["apple", "fruit", "red", "healthy"]},
{"shortcode": "banana", "emoji": "🍌", "keywords": ["banana", "fruit", "yellow", "monkey"]},
# Animals self.emoji_list.append({
{"shortcode": "cat", "emoji": "🐱", "keywords": ["cat", "kitten", "meow", "feline"]}, "shortcode": shortcode,
{"shortcode": "dog", "emoji": "🐶", "keywords": ["dog", "puppy", "woof", "canine"]}, "emoji": emoji_char,
{"shortcode": "mouse", "emoji": "🐭", "keywords": ["mouse", "small", "rodent", "squeak"]}, "keywords": keywords
{"shortcode": "bear", "emoji": "🐻", "keywords": ["bear", "large", "forest", "cute"]}, })
{"shortcode": "panda", "emoji": "🐼", "keywords": ["panda", "bear", "black", "white"]},
{"shortcode": "lion", "emoji": "🦁", "keywords": ["lion", "king", "mane", "roar"]}, # Loaded successfully
{"shortcode": "tiger", "emoji": "🐯", "keywords": ["tiger", "stripes", "orange", "wild"]},
{"shortcode": "fox", "emoji": "🦊", "keywords": ["fox", "red", "clever", "sly"]},
{"shortcode": "wolf", "emoji": "🐺", "keywords": ["wolf", "pack", "howl", "wild"]},
{"shortcode": "unicorn", "emoji": "🦄", "keywords": ["unicorn", "magic", "rainbow", "fantasy"]},
# Activities and celebrations
{"shortcode": "party", "emoji": "🎉", "keywords": ["party", "celebration", "confetti", "fun"]},
{"shortcode": "birthday", "emoji": "🎂", "keywords": ["birthday", "cake", "celebration", "age"]},
{"shortcode": "gift", "emoji": "🎁", "keywords": ["gift", "present", "box", "surprise"]},
{"shortcode": "balloon", "emoji": "🎈", "keywords": ["balloon", "party", "float", "celebration"]},
{"shortcode": "music", "emoji": "🎵", "keywords": ["music", "notes", "song", "melody"]},
{"shortcode": "dance", "emoji": "💃", "keywords": ["dance", "woman", "party", "fun"]},
# Halloween and seasonal
{"shortcode": "jack_o_lantern", "emoji": "🎃", "keywords": ["jack", "lantern", "pumpkin", "halloween"]},
{"shortcode": "ghost", "emoji": "👻", "keywords": ["ghost", "spooky", "halloween", "boo"]},
{"shortcode": "skull", "emoji": "💀", "keywords": ["skull", "death", "spooky", "halloween"]},
{"shortcode": "spider", "emoji": "🕷️", "keywords": ["spider", "web", "spooky", "halloween"]},
{"shortcode": "bat", "emoji": "🦇", "keywords": ["bat", "fly", "night", "halloween"]},
{"shortcode": "christmas_tree", "emoji": "🎄", "keywords": ["christmas", "tree", "holiday", "winter"]},
{"shortcode": "santa", "emoji": "🎅", "keywords": ["santa", "christmas", "holiday", "ho"]},
{"shortcode": "snowman", "emoji": "", "keywords": ["snowman", "winter", "cold", "carrot"]},
# Technology
{"shortcode": "computer", "emoji": "💻", "keywords": ["computer", "laptop", "tech", "work"]},
{"shortcode": "phone", "emoji": "📱", "keywords": ["phone", "mobile", "cell", "smartphone"]},
{"shortcode": "camera", "emoji": "📷", "keywords": ["camera", "photo", "picture", "snap"]},
{"shortcode": "video", "emoji": "📹", "keywords": ["video", "camera", "record", "film"]},
{"shortcode": "robot", "emoji": "🤖", "keywords": ["robot", "ai", "artificial", "intelligence"]},
# Transportation
{"shortcode": "car", "emoji": "🚗", "keywords": ["car", "drive", "vehicle", "auto"]},
{"shortcode": "bike", "emoji": "🚲", "keywords": ["bike", "bicycle", "ride", "cycle"]},
{"shortcode": "plane", "emoji": "✈️", "keywords": ["plane", "airplane", "fly", "travel"]},
{"shortcode": "rocket", "emoji": "🚀", "keywords": ["rocket", "space", "launch", "fast"]},
]
def set_mention_list(self, mentions: List[str]): def set_mention_list(self, mentions: List[str]):
"""Set the list of available mentions (usernames)""" """Set the list of available mentions (usernames)"""
@ -195,7 +146,7 @@ class AutocompleteTextEdit(QTextEdit):
# Look backwards for @ or : # Look backwards for @ or :
start_pos = pos - 1 start_pos = pos - 1
while start_pos >= 0 and text[start_pos] not in [' ', '\n', '\t']: while start_pos >= 0 and start_pos < len(text) and text[start_pos] not in [' ', '\n', '\t']:
start_pos -= 1 start_pos -= 1
start_pos += 1 start_pos += 1
@ -255,6 +206,7 @@ class AutocompleteTextEdit(QTextEdit):
any(keyword.startswith(prefix_lower) for keyword in keywords)): any(keyword.startswith(prefix_lower) for keyword in keywords)):
display_text = f"{shortcode} {emoji['emoji']}" display_text = f"{shortcode} {emoji['emoji']}"
matches.append(display_text) matches.append(display_text)
# Add to matches
if matches: if matches:
self.show_completer(matches, prefix, start_pos, 'emoji') self.show_completer(matches, prefix, start_pos, 'emoji')
@ -291,7 +243,12 @@ class AutocompleteTextEdit(QTextEdit):
def hide_completer(self): def hide_completer(self):
"""Hide the completer""" """Hide the completer"""
if self.completer: if self.completer:
self.completer.popup().hide() popup = self.completer.popup()
popup.hide()
# Clear the completion state
self.completion_prefix = ""
self.completion_start = 0
self.completion_type = None
def insert_completion(self): def insert_completion(self):
"""Insert the selected completion""" """Insert the selected completion"""
@ -303,7 +260,8 @@ class AutocompleteTextEdit(QTextEdit):
if not current_index.isValid(): if not current_index.isValid():
return return
completion = self.completer.currentCompletion() # Get the actually selected completion from the popup
completion = current_index.data()
# Replace the current prefix with the completion # Replace the current prefix with the completion
cursor = self.textCursor() cursor = self.textCursor()
@ -313,12 +271,43 @@ class AutocompleteTextEdit(QTextEdit):
if self.completion_type == 'mention': if self.completion_type == 'mention':
cursor.insertText(f"@{completion} ") cursor.insertText(f"@{completion} ")
elif self.completion_type == 'emoji': elif self.completion_type == 'emoji':
# Extract just the shortcode (before the emoji) # Extract the actual emoji character (after the shortcode)
shortcode = completion.split()[0] parts = completion.split(' ', 1)
cursor.insertText(f":{shortcode}: ") if len(parts) >= 2:
emoji_char = parts[1].strip()
cursor.insertText(f"{emoji_char} ")
else:
# Fallback to shortcode format if parsing fails
shortcode = parts[0]
cursor.insertText(f":{shortcode}: ")
# Set the updated cursor position
self.setTextCursor(cursor)
self.hide_completer() self.hide_completer()
# Try immediate focus restoration
self.setFocus(Qt.OtherFocusReason)
# Also try with a delay as backup
from PySide6.QtCore import QTimer
QTimer.singleShot(0, self.restore_focus_immediate)
QTimer.singleShot(100, self.restore_focus)
def restore_focus_immediate(self):
"""Immediate focus restoration attempt"""
self.setFocus(Qt.TabFocusReason)
self.ensureCursorVisible()
def restore_focus(self):
"""Delayed focus restoration as backup"""
# Ensure we're visible and enabled before taking focus
if self.isVisible() and self.isEnabled():
self.setFocus(Qt.TabFocusReason) # Use TabFocusReason instead
self.activateWindow() # Also activate the parent window
self.ensureCursorVisible()
# Force a repaint to ensure visual focus
self.update()
def update_mention_list(self, mentions: List[str]): def update_mention_list(self, mentions: List[str]):
"""Update mention list (called from parent when data is ready)""" """Update mention list (called from parent when data is ready)"""
self.mention_list = mentions self.mention_list = mentions

View File

@ -223,20 +223,92 @@ class ComposeDialog(QDialog):
def load_mention_suggestions(self, prefix: str): def load_mention_suggestions(self, prefix: str):
"""Load mention suggestions based on prefix""" """Load mention suggestions based on prefix"""
# TODO: Implement fetching followers/following from API try:
# For now, use expanded sample suggestions with realistic fediverse usernames # Get the active account and create API client
sample_mentions = [ active_account = self.account_manager.get_active_account()
"alice", "bob", "charlie", "diana", "eve", "frank", "grace", "henry", "ivy", "jack", if not active_account:
"admin", "moderator", "announcements", "news", "updates", "support", "help", return
"alex_dev", "jane_artist", "mike_writer", "sarah_photographer", "tom_musician",
"community", "local_news", "tech_updates", "fedi_tips", "open_source", from activitypub.client import ActivityPubClient
"cat_lover", "dog_walker", "book_reader", "movie_fan", "game_dev", "web_designer", client = ActivityPubClient(active_account.instance_url, active_account.access_token)
"climate_activist", "space_enthusiast", "food_blogger", "travel_tales", "art_gallery"
] # Get current user's account ID
current_user = client.verify_credentials()
# Filter by prefix (case insensitive) current_account_id = current_user['id']
filtered = [name for name in sample_mentions if name.lower().startswith(prefix.lower())]
self.text_edit.update_mention_list(filtered) # Collect usernames from multiple sources
usernames = set()
# 1. Search for accounts matching the prefix
if len(prefix) >= 1: # Search when user has typed at least 1 character
try:
search_results = client.search_accounts(prefix, limit=10)
for account in search_results:
# Use full fediverse handle (acct field) or construct it
full_handle = account.get('acct', '')
if not full_handle:
username = account.get('username', '')
domain = account.get('url', '').split('/')[2] if account.get('url') else ''
if username and domain:
full_handle = f"{username}@{domain}"
else:
full_handle = username
if full_handle:
usernames.add(full_handle)
except Exception as e:
print(f"Search failed: {e}")
# 2. Get followers (people who follow you)
try:
followers = client.get_followers(current_account_id, limit=50)
for follower in followers:
# Use full fediverse handle
full_handle = follower.get('acct', '')
if not full_handle:
username = follower.get('username', '')
domain = follower.get('url', '').split('/')[2] if follower.get('url') else ''
if username and domain:
full_handle = f"{username}@{domain}"
else:
full_handle = username
if full_handle and full_handle.lower().startswith(prefix.lower()):
usernames.add(full_handle)
except Exception as e:
print(f"Failed to get followers: {e}")
# 3. Get following (people you follow)
try:
following = client.get_following(current_account_id, limit=50)
for account in following:
# Use full fediverse handle
full_handle = account.get('acct', '')
if not full_handle:
username = account.get('username', '')
domain = account.get('url', '').split('/')[2] if account.get('url') else ''
if username and domain:
full_handle = f"{username}@{domain}"
else:
full_handle = username
if full_handle and full_handle.lower().startswith(prefix.lower()):
usernames.add(full_handle)
except Exception as e:
print(f"Failed to get following: {e}")
# Convert to sorted list
filtered = sorted(list(usernames))
# Only use real API data - no fallback to incomplete sample data
# The empty list will trigger a fresh API call if needed
self.text_edit.update_mention_list(filtered)
except Exception as e:
print(f"Failed to load mention suggestions: {e}")
# Fallback to empty list
self.text_edit.update_mention_list([])
def load_emoji_suggestions(self, prefix: str): def load_emoji_suggestions(self, prefix: str):
"""Load emoji suggestions based on prefix""" """Load emoji suggestions based on prefix"""