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:
39
README.md
39
README.md
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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"""
|
||||||
|
Reference in New Issue
Block a user