Skip to content

Commit b760d10

Browse files
committed
final fix hopefully
1 parent df8fb8d commit b760d10

11 files changed

Lines changed: 133 additions & 53 deletions

File tree

backend/routes/chat_rooms.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,11 @@ def convert_objectids(obj):
812812
socketio.emit('new_chat_room_message', new_message, room=room_name)
813813
logger.info(f"Emitted new_chat_room_message event to room {room_name} for message {message_id}")
814814
# Also emit to sender's personal room so the sender always sees the message (even if not joined)
815-
try:
816-
socketio.emit('new_chat_room_message', new_message, room=f"user_{user_id}")
817-
except Exception:
818-
pass
815+
# FIX: Removed - this causes double messages for the sender if they are already in the room
816+
# try:
817+
# socketio.emit('new_chat_room_message', new_message, room=f"user_{user_id}")
818+
# except Exception:
819+
# pass
819820
except Exception as e:
820821
logger.warning(f"Failed to emit socket event for message {message_id}: {str(e)}")
821822

@@ -832,23 +833,27 @@ def convert_objectids(obj):
832833
members = [str(m) for m in room_data.get('members', [])] if room_data else []
833834
room_name = room.get('name', 'Chat Room')
834835

836+
# Check for @everyone or @todos
837+
is_everyone_mention = ('@everyone' in content or '@todos' in content) and not room.get('is_public', True)
838+
835839
# Notify each member (except the sender)
836840
for member_id in members:
837841
if member_id == user_id:
838842
continue # Don't notify the sender
839843

840844
# Check if topic is muted FIRST (topic silencing overrides everything)
841-
if topic_id and notification_settings.is_topic_muted(member_id, topic_id):
845+
# @everyone bypasses mute settings in private chats usually, but let's respect topic mute if critical
846+
if not is_everyone_mention and topic_id and notification_settings.is_topic_muted(member_id, topic_id):
842847
logger.info(f"Skipping notification for member {member_id} - topic {topic_id} is muted")
843848
continue
844849

845850
# Check if chat room itself is muted
846-
if notification_settings.is_chat_room_muted(member_id, room_id):
851+
if not is_everyone_mention and notification_settings.is_chat_room_muted(member_id, room_id):
847852
logger.info(f"Skipping notification for member {member_id} - chatroom {room_id} is muted")
848853
continue
849854

850855
# Check if user is following the chatroom
851-
if not notification_settings.is_following_chat_room(member_id, room_id):
856+
if not is_everyone_mention and not notification_settings.is_following_chat_room(member_id, room_id):
852857
logger.info(f"Skipping notification for member {member_id} - chatroom {room_id} is not followed")
853858
continue
854859

frontend/components/ChatRoom/ChatRoomContainer.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import MessageDeleteDialog from '@/components/UI/MessageDeleteDialog';
2020
import MessageContextMenu from '@/components/UI/MessageContextMenu';
2121
import ChatIcon from '@/components/UI/ChatIcon';
2222
import ChatRoomContextMenu from '@/components/UI/ChatRoomContextMenu';
23-
import { VoipButton, VoipControlBar } from '@/components/Voip';
23+
import { VoipButton } from '@/components/Voip';
24+
import { Mic, Send, Bell, BellOff, Volume2, VolumeX, Trash2, Image, Paperclip, Share2, Square, X } from 'lucide-react';
25+
import AudioPlayer from '@/components/UI/AudioPlayer';
2426
import { api, API_ENDPOINTS } from '@/utils/api';
2527
import { toast } from 'react-hot-toast';
2628
import { getAnonymousModeState, saveAnonymousModeState, getLastAnonymousName } from '@/utils/anonymousStorage';
2729
import { analyzeImageBrightness, getTextColorClass } from '@/utils/imageBrightness';
28-
import AudioPlayer from '@/components/UI/AudioPlayer';
29-
import { Mic, Square, Send, X, Trash2, Image, Paperclip, Share2, Bell, BellOff } from 'lucide-react';
3030

3131
interface Message {
3232
id: string;
@@ -177,8 +177,9 @@ const ChatRoomContainer: React.FC<ChatRoomContainerProps> = ({
177177
const [mentionUsers, setMentionUsers] = useState<Array<{ id: string, username: string }>>([]);
178178
const [showMentionDropdown, setShowMentionDropdown] = useState(false);
179179
const [mentionSearch, setMentionSearch] = useState('');
180-
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0);
180+
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0); // State for mention selection
181181
const [mentionCursorPosition, setMentionCursorPosition] = useState(0);
182+
const [hiddenMessageIds, setHiddenMessageIds] = useState<string[]>([]);
182183

183184
// Debounced global search for mentions
184185
useEffect(() => {
@@ -802,7 +803,7 @@ const ChatRoomContainer: React.FC<ChatRoomContainerProps> = ({
802803
}
803804

804805
const response = await api.post(API_ENDPOINTS.CHAT_ROOMS.SEND_MESSAGE(room.id), {
805-
content: messageInput.trim() || (selectedGifUrl ? '[GIF]' : '') || (audioBlob ? (t('voip.audioMessage') || 'Voice Message') : '') || (attachments.length > 0 ? '[Attachment]' : ''),
806+
content: messageInput.trim(),
806807
message_type: messageType,
807808
gif_url: selectedGifUrl,
808809
attachments: attachments.length > 0 ? attachments : undefined,
@@ -1082,7 +1083,7 @@ const ChatRoomContainer: React.FC<ChatRoomContainerProps> = ({
10821083
<p>{t('chat.noMessages')}</p>
10831084
</div>
10841085
) : (
1085-
messages.map(message => (
1086+
messages.filter(m => !hiddenMessageIds.includes(m.id)).map(message => (
10861087
<div
10871088
key={message.id}
10881089
className="flex gap-3"
@@ -1221,6 +1222,15 @@ const ChatRoomContainer: React.FC<ChatRoomContainerProps> = ({
12211222
}}
12221223
/>
12231224
);
1225+
} else if (attachment.type === 'audio') {
1226+
return (
1227+
<div key={idx} className="w-full max-w-md min-w-[200px]">
1228+
<AudioPlayer
1229+
src={attachment.url}
1230+
filename={attachment.filename}
1231+
/>
1232+
</div>
1233+
);
12241234
} else {
12251235
return (
12261236
<FileCard
@@ -1653,6 +1663,10 @@ const ChatRoomContainer: React.FC<ChatRoomContainerProps> = ({
16531663
x={messageContextMenu.x}
16541664
y={messageContextMenu.y}
16551665
onClose={() => setMessageContextMenu(null)}
1666+
onHide={() => {
1667+
setHiddenMessageIds(prev => [...prev, messageContextMenu.messageId]);
1668+
setMessageContextMenu(null);
1669+
}}
16561670
onReportMessage={(messageId, userId, username) => {
16571671
if (userId && username) {
16581672
handleReportUserFromMessage(messageId, userId, username);

frontend/components/Layout/Layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ const Layout: React.FC<LayoutProps> = ({ children, transparentHeader = false })
256256
}}
257257
>
258258
<span className="text-sm font-medium theme-text-primary">{t('userMenu.settings') || 'Settings'}</span>
259-
<div className="p-2 rounded-full theme-bg-secondary">
259+
<div className="p-2 rounded-lg theme-bg-secondary">
260260
<Settings size={18} className="theme-text-primary" />
261261
</div>
262262
</button>
@@ -297,7 +297,7 @@ const Layout: React.FC<LayoutProps> = ({ children, transparentHeader = false })
297297
className="w-full flex items-center justify-between p-2 rounded-lg hover:theme-bg-tertiary transition-colors text-left"
298298
>
299299
<span className="text-sm font-medium theme-text-primary">{t('supportWidget.title') || 'Support'}</span>
300-
<div className="p-2 rounded-full theme-bg-secondary">
300+
<div className="p-2 rounded-lg theme-bg-secondary">
301301
<LifeBuoy size={18} className="theme-text-primary" />
302302
</div>
303303
</button>

frontend/components/PrivateMessages/PrivateMessagesSimplified.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,10 +1212,7 @@ const PrivateMessagesSimplified: React.FC<PrivateMessagesSimplifiedProps> = ({
12121212
}
12131213
}
12141214

1215-
const attachmentPlaceholder = `[${t('privateMessages.attachment')}]`;
1216-
const audioPlaceholder = t('voip.audioMessage') || 'Voice Message';
1217-
1218-
const finalContent = content || (messageType === 'gif' ? '[GIF]' : '') || (messageType === 'audio' ? audioPlaceholder : '') || (attachments.length > 0 ? attachmentPlaceholder : '');
1215+
const finalContent = content;
12191216

12201217
const response = await api.post(API_ENDPOINTS.MESSAGES.SEND_PRIVATE, {
12211218
to_user_id: selectedConversation,

frontend/components/Settings/MutedItemsModal.tsx

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ const MutedItemsModal: React.FC<MutedItemsModalProps> = ({ isOpen, onClose }) =>
9797
};
9898

9999
const formatRemainingTime = (until: string | null | undefined) => {
100-
if (!until) return t('mute.forever') || 'Forever';
100+
if (!until) return t('settings.mute.forever') || 'Forever';
101101
const date = new Date(until);
102102
const now = new Date();
103-
if (date <= now) return t('mute.expired') || 'Expired';
103+
if (date <= now) return t('settings.mute.expired') || 'Expired';
104104

105105
const diff = date.getTime() - now.getTime();
106106
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
@@ -114,6 +114,27 @@ const MutedItemsModal: React.FC<MutedItemsModalProps> = ({ isOpen, onClose }) =>
114114

115115
if (!isOpen) return null;
116116

117+
const getTypeLabel = (type: string) => {
118+
switch (type) {
119+
case 'topic': return t('settings.mute.types.topic') || 'Topic';
120+
case 'chatroom': return t('settings.mute.types.chatroom') || 'Chatroom';
121+
case 'post': return t('settings.mute.types.post') || 'Post';
122+
default: return type;
123+
}
124+
};
125+
126+
const getTypeIcon = (type: string) => {
127+
// You might need to import these icons if they aren't already available
128+
// assuming standard lucide-react imports
129+
return (
130+
<svg className="w-3 h-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
131+
{type === 'topic' && <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14" />}
132+
{type === 'chatroom' && <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />}
133+
{type === 'post' && <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />}
134+
</svg>
135+
);
136+
};
137+
117138
return (
118139
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" onClick={onClose}>
119140
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] flex flex-col" onClick={(e) => e.stopPropagation()}>
@@ -155,36 +176,37 @@ const MutedItemsModal: React.FC<MutedItemsModalProps> = ({ isOpen, onClose }) =>
155176
<div key={`${item.type}-${item.id}`} className="p-3 rounded-lg border theme-border theme-bg-tertiary flex items-center justify-between">
156177
<div className="flex-1 min-w-0 pr-4">
157178
<div className="flex items-center gap-2 mb-1">
158-
<span className={`text-xs px-2 py-0.5 rounded-full uppercase font-bold tracking-wider
159-
${item.type === 'topic' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :
160-
item.type === 'chatroom' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
161-
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'
179+
<span className={`inline-flex items-center text-xs px-2.5 py-1 rounded-full uppercase font-bold tracking-wider
180+
${item.type === 'topic' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200' :
181+
item.type === 'chatroom' ? 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-200' :
182+
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-200'
162183
}`}>
163-
{item.type}
184+
{getTypeIcon(item.type)}
185+
{getTypeLabel(item.type)}
164186
</span>
165-
<h3 className="font-medium theme-text-primary truncate">{item.name}</h3>
187+
<h3 className="font-medium theme-text-primary truncate ml-1">{item.name}</h3>
166188
</div>
167-
<div className="flex items-center gap-4 text-xs theme-text-muted">
189+
<div className="flex items-center gap-4 text-xs theme-text-muted mt-2">
168190
{item.topic_title && (
169-
<span className="flex items-center gap-1 font-medium bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded text-gray-600 dark:text-gray-300">
170-
in {item.topic_title}
191+
<span className="flex items-center gap-1 font-medium bg-gray-100 dark:bg-gray-700/50 px-2 py-0.5 rounded text-gray-600 dark:text-gray-300">
192+
{t('settings.mute.in', { context: item.topic_title }) || `in ${item.topic_title}`}
171193
</span>
172194
)}
173195
{item.silenced_until && (
174196
<span className="flex items-center gap-1 text-orange-500">
175197
<Clock size={12} />
176-
{formatRemainingTime(item.silenced_until)} remaining
198+
{formatRemainingTime(item.silenced_until)} {t('settings.mute.remaining') || 'remaining'}
177199
</span>
178200
)}
179-
{!item.silenced_until && <span>Indefinitely</span>}
201+
{!item.silenced_until && <span>{t('settings.mute.indefinitely') || 'Indefinitely'}</span>}
180202
</div>
181203
</div>
182204
<button
183205
onClick={() => handleUnmute(item)}
184206
disabled={processingId === item.id}
185-
className="px-3 py-1.5 text-xs font-medium bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors"
207+
className="px-3 py-1.5 text-xs font-medium bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
186208
>
187-
{processingId === item.id ? <LoadingSpinner size="sm" /> : (t('common.unmute') || 'Unmute')}
209+
{processingId === item.id ? <LoadingSpinner size="sm" /> : (t('mute.unmute') || 'Unmute')}
188210
</button>
189211
</div>
190212
))}

frontend/components/UI/UserBanner.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ const UserBanner: React.FC<UserBannerProps> = ({
6868
return;
6969
}
7070

71+
// Handle @everyone and @todos special mentions
72+
if (initialUsername === 'everyone' || initialUsername === 'todos') {
73+
setUser({
74+
id: '',
75+
username: initialUsername === 'everyone' ? (t('chat.tagEveryone') || 'Everyone') : 'Todos',
76+
// Optional: Set a specific banner or leave empty to use gradient
77+
created_at: new Date().toISOString(),
78+
});
79+
setLoading(false);
80+
return;
81+
}
82+
7183
if (!userId && !initialUsername) {
7284
setError(t('errors.userNotFound') || 'User not found');
7385
setLoading(false);

frontend/contexts/SocketContext.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,31 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ children }) => {
6666
// On server: use BACKEND_IP or NEXT_PUBLIC_API_URL
6767
let socketUrl: string;
6868
if (typeof window !== 'undefined') {
69-
const isProduction = window.location.hostname === 'topicsflow.me' ||
70-
window.location.hostname === 'www.topicsflow.me' ||
71-
window.location.hostname.includes('azurestaticapps.net');
69+
const isProduction = window.location.hostname === 'topicsflow.me' ||
70+
window.location.hostname === 'www.topicsflow.me' ||
71+
window.location.hostname.includes('azurestaticapps.net');
72+
73+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
74+
7275
const backendUrl = process.env.BACKEND_IP || process.env.NEXT_PUBLIC_API_URL;
73-
74-
if (isProduction && backendUrl) {
75-
// Production: Use backend URL directly
76-
socketUrl = backendUrl;
77-
} else if (isProduction) {
78-
// Production but no backend URL: use api.topicsflow.me as default
79-
socketUrl = 'https://api.topicsflow.me';
76+
77+
if (isProduction) {
78+
// Production: Always use backend URL or default to api.topicsflow.me
79+
if (backendUrl) {
80+
socketUrl = backendUrl;
81+
} else {
82+
socketUrl = 'https://api.topicsflow.me';
83+
}
84+
} else if (isLocalhost) {
85+
// Local Development: Prioritize localhost:5000
86+
if (backendUrl && (backendUrl.includes('topicsflow.me') || backendUrl.includes('azurestaticapps.net'))) {
87+
socketUrl = 'http://localhost:5000';
88+
console.warn('[SocketContext] Environment variables point to production but running on localhost. Forcing socket to http://localhost:5000');
89+
} else {
90+
socketUrl = backendUrl || 'http://localhost:5000';
91+
}
8092
} else {
81-
// Local Development: Always use localhost:5000 directly (no proxy)
93+
// Other environments
8294
socketUrl = backendUrl || 'http://localhost:5000';
8395
}
8496
} else {

frontend/locales/en.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1555,7 +1555,16 @@
15551555
"24hours": "24 hours",
15561556
"forever": "Until I turn it back on",
15571557
"silenced": "Topic silenced",
1558-
"unsilenced": "Topic unsilenced"
1558+
"unsilenced": "Topic unsilenced",
1559+
"expired": "Expired",
1560+
"remaining": "remaining",
1561+
"indefinitely": "Indefinitely",
1562+
"in": "in {context}",
1563+
"types": {
1564+
"topic": "Topic",
1565+
"chatroom": "Chatroom",
1566+
"post": "Post"
1567+
}
15591568
},
15601569
"messages": {
15611570
"deleteMessage": "Delete Message",

frontend/locales/pt.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1536,7 +1536,16 @@
15361536
"24hours": "24 horas",
15371537
"forever": "Até eu voltar a ligar",
15381538
"silenced": "Tópico silenciado",
1539-
"unsilenced": "Tópico não silenciado"
1539+
"unsilenced": "Tópico não silenciado",
1540+
"expired": "Expirado",
1541+
"remaining": "restantes",
1542+
"indefinitely": "Indefinidamente",
1543+
"in": "em {context}",
1544+
"types": {
1545+
"topic": "Tópico",
1546+
"chatroom": "Sala de Chat",
1547+
"post": "Publicação"
1548+
}
15401549
},
15411550
"userBanner": {
15421551
"joined": "Entrou",

0 commit comments

Comments
 (0)