Slack has a powerful @mentions and notifications system - alerts for messages, threads, and mentions.
1interface Mention {
2 type: 'user' | 'channel' | 'here' | 'everyone';
3 userId?: string;
4 position: { start: number; end: number }; // in content string
5}
6
7// Parsing mentions from message content
8function parseMentions(content: string, members: Member[]): {
9 parsed: string;
10 mentions: Mention[];
11} {
12 const mentions: Mention[] = [];
13 let parsed = content;
14
15 // @user mentions
16 const userMentionRegex = /@(w+)/g;
17 let match;
18
19 while ((match = userMentionRegex.exec(content)) !== null) {
20 const username = match[1];
21 const member = members.find(m =>
22 m.name.toLowerCase().replace(/s+/g, '') === username.toLowerCase()
23 );
24
25 if (member) {
26 mentions.push({
27 type: 'user',
28 userId: member.id,
29 position: { start: match.index, end: match.index + match[0].length }
30 });
31 }
32 }
33
34 // @channel, @here, @everyone
35 if (content.includes('@channel')) {
36 mentions.push({ type: 'channel', position: { start: 0, end: 0 } });
37 }
38 if (content.includes('@here')) {
39 mentions.push({ type: 'here', position: { start: 0, end: 0 } });
40 }
41 if (content.includes('@everyone')) {
42 mentions.push({ type: 'everyone', position: { start: 0, end: 0 } });
43 }
44
45 return { parsed, mentions };
46}
47
48// Rendering mentions in the UI
49function renderContent(content: string, mentions: Mention[], members: Member[]) {
50 if (mentions.length === 0) return content;
51
52 const parts: React.ReactNode[] = [];
53 let lastIndex = 0;
54
55 // Sort mentions by position
56 const sortedMentions = [...mentions].sort((a, b) => a.position.start - b.position.start);
57
58 sortedMentions.forEach((mention, idx) => {
59 // Add text before mention
60 if (mention.position.start > lastIndex) {
61 parts.push(content.substring(lastIndex, mention.position.start));
62 }
63
64 // Add mention
65 const mentionText = content.substring(mention.position.start, mention.position.end);
66 parts.push(
67 <span key={idx} className="bg-blue-100 text-blue-700 px-1 rounded font-semibold">
68 {mentionText}
69 </span>
70 );
71
72 lastIndex = mention.position.end;
73 });
74
75 // Add remaining text
76 if (lastIndex < content.length) {
77 parts.push(content.substring(lastIndex));
78 }
79
80 return <>{parts}</>;
81}1'use client';
2
3import { useState, useEffect, useRef } from 'react';
4
5interface MentionAutocompleteProps {
6 members: Member[];
7 onSelect: (member: Member) => void;
8 trigger: string; // "@" character position
9 cursorPosition: number;
10}
11
12export default function MentionAutocomplete({
13 members,
14 onSelect,
15 trigger,
16 cursorPosition
17}: MentionAutocompleteProps) {
18 const [query, setQuery] = useState('');
19 const [selectedIndex, setSelectedIndex] = useState(0);
20
21 const filteredMembers = members.filter(member =>
22 member.name.toLowerCase().includes(query.toLowerCase())
23 ).slice(0, 5);
24
25 return (
26 <div className="absolute bottom-full left-0 mb-2 bg-white border rounded-lg shadow-lg w-64 max-h-64 overflow-y-auto">
27 {filteredMembers.length === 0 ? (
28 <div className="p-3 text-sm text-gray-500">No matches found</div>
29 ) : (
30 filteredMembers.map((member, idx) => (
31 <button
32 key={member.id}
33 onClick={() => onSelect(member)}
34 className={`w-full flex items-center gap-3 px-3 py-2 hover:bg-blue-50 ${
35 idx === selectedIndex ? 'bg-blue-50' : ''
36 }`}
37 >
38 <div className="relative">
39 <img
40 src={member.avatar}
41 alt={member.name}
42 className="w-8 h-8 rounded"
43 />
44 <div
45 className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border border-white ${
46 member.status === 'online' ? 'bg-green-400' :
47 member.status === 'away' ? 'bg-yellow-400' : 'bg-gray-400'
48 }`}
49 />
50 </div>
51 <div className="flex-1 text-left">
52 <div className="font-semibold text-sm">{member.name}</div>
53 <div className="text-xs text-gray-600 truncate">
54 {member.statusText || member.role}
55 </div>
56 </div>
57 </button>
58 ))
59 )}
60
61 <div className="border-t p-2 text-xs text-gray-500">
62 <div className="flex items-center gap-2">
63 <span>@channel</span>
64 <span className="text-gray-400">•</span>
65 <span>notify all members</span>
66 </div>
67 <div className="flex items-center gap-2 mt-1">
68 <span>@here</span>
69 <span className="text-gray-400">•</span>
70 <span>notify active members</span>
71 </div>
72 </div>
73 </div>
74 );
75}1interface Notification {
2 id: string;
3 type: 'mention' | 'reply' | 'reaction' | 'dm';
4
5 messageId: string;
6 channelId: string;
7 channelName: string;
8
9 from: {
10 id: string;
11 name: string;
12 avatar: string;
13 };
14
15 preview: string; // message preview
16 timestamp: Date;
17 isRead: boolean;
18}
19
20interface NotificationCenterProps {
21 notifications: Notification[];
22 onMarkAsRead: (notificationId: string) => void;
23 onMarkAllAsRead: () => void;
24 onNavigate: (channelId: string, messageId: string) => void;
25}
26
27export default function NotificationCenter({
28 notifications,
29 onMarkAsRead,
30 onMarkAllAsRead,
31 onNavigate
32}: NotificationCenterProps) {
33 const unreadCount = notifications.filter(n => !n.isRead).length;
34
35 const getNotificationIcon = (type: Notification['type']) => {
36 switch (type) {
37 case 'mention': return '@';
38 case 'reply': return '💬';
39 case 'reaction': return '😊';
40 case 'dm': return '💌';
41 }
42 };
43
44 return (
45 <div className="w-96 bg-white border-l flex flex-col h-screen">
46 {/* Header */}
47 <div className="p-4 border-b">
48 <div className="flex items-center justify-between mb-3">
49 <h2 className="text-xl font-bold">Notifications</h2>
50 {unreadCount > 0 && (
51 <button
52 onClick={onMarkAllAsRead}
53 className="text-sm text-blue-600 hover:underline"
54 >
55 Mark all as read
56 </button>
57 )}
58 </div>
59
60 {unreadCount > 0 && (
61 <div className="text-sm text-gray-600">
62 {unreadCount} unread {unreadCount === 1 ? 'notification' : 'notifications'}
63 </div>
64 )}
65 </div>
66
67 {/* Notifications List */}
68 <div className="flex-1 overflow-y-auto">
69 {notifications.length === 0 ? (
70 <div className="p-8 text-center text-gray-500">
71 <div className="text-4xl mb-2">🔔</div>
72 <p>No notifications yet</p>
73 </div>
74 ) : (
75 notifications.map(notification => (
76 <div
77 key={notification.id}
78 onClick={() => {
79 onNavigate(notification.channelId, notification.messageId);
80 if (!notification.isRead) {
81 onMarkAsRead(notification.id);
82 }
83 }}
84 className={`p-4 border-b cursor-pointer hover:bg-gray-50 ${
85 !notification.isRead ? 'bg-blue-50' : ''
86 }`}
87 >
88 <div className="flex gap-3">
89 <div className="relative">
90 <img
91 src={notification.from.avatar}
92 alt={notification.from.name}
93 className="w-10 h-10 rounded"
94 />
95 <div className="absolute -bottom-1 -right-1 w-5 h-5 bg-white rounded-full flex items-center justify-center border">
96 <span className="text-xs">
97 {getNotificationIcon(notification.type)}
98 </span>
99 </div>
100 </div>
101
102 <div className="flex-1 min-w-0">
103 <div className="flex items-start justify-between mb-1">
104 <div className="font-semibold text-sm">
105 {notification.from.name}
106 {notification.type === 'mention' && ' mentioned you'}
107 {notification.type === 'reply' && ' replied to your thread'}
108 {notification.type === 'reaction' && ' reacted to your message'}
109 {notification.type === 'dm' && ' sent you a message'}
110 </div>
111 {!notification.isRead && (
112 <div className="w-2 h-2 bg-blue-600 rounded-full flex-shrink-0 mt-1"></div>
113 )}
114 </div>
115
116 <div className="text-sm text-gray-600 mb-1">
117 #{notification.channelName}
118 </div>
119
120 <div className="text-sm text-gray-700 truncate">
121 {notification.preview}
122 </div>
123
124 <div className="text-xs text-gray-500 mt-1">
125 {formatTimeAgo(notification.timestamp)}
126 </div>
127 </div>
128 </div>
129 </div>
130 ))
131 )}
132 </div>
133 </div>
134 );
135}
136
137function formatTimeAgo(date: Date): string {
138 const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
139
140 if (seconds < 60) return 'just now';
141 if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
142 if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
143 if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
144
145 return date.toLocaleDateString();
146}1interface UnreadBadgeProps {
2 count: number;
3 isMention?: boolean; // special styling for mentions
4}
5
6export default function UnreadBadge({ count, isMention }: UnreadBadgeProps) {
7 if (count === 0) return null;
8
9 return (
10 <span
11 className={`ml-auto px-2 py-0.5 rounded-full text-xs font-bold ${
12 isMention
13 ? 'bg-red-500 text-white'
14 : 'bg-gray-500 text-white'
15 }`}
16 >
17 {count > 99 ? '99+' : count}
18 </span>
19 );
20}✅ @mentions - parse and highlight in messages ✅ Mention autocomplete - dropdown with members list ✅ @channel, @here, @everyone - special mentions ✅ Notification center - central place for all notifications ✅ Notification types - mention, reply, reaction, DM ✅ Unread badges - counts in the sidebar ✅ Mark as read - single and bulk actions ✅ Navigate to message - clicking a notification jumps to the message
You now have a complete Slack clone with workspace, channels, messages, threads, mentions, and notifications! 💬
See you next time! 🚀