We use cookies to enhance your experience on the site
CodeWorlds

Mentions and Notifications - Alerts

Slack has a powerful @mentions and notifications system - alerts for messages, threads, and mentions.

Mention System

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}

Mention Autocomplete

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}

Notification System

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}

Unread Badge

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}

Summary 🎓

@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! 🚀

Go to CodeWorlds