tacel-chat 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Formatting utilities for tacel-chat
3
+ */
4
+
5
+ /**
6
+ * Format a timestamp for display in the chat list
7
+ * Today: "3:45 PM", Yesterday: "Yesterday", Older: "Jan 5"
8
+ */
9
+ function formatChatListTime(timestamp) {
10
+ if (!timestamp) return '';
11
+ const date = new Date(timestamp);
12
+ if (isNaN(date.getTime())) return '';
13
+
14
+ const now = new Date();
15
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
16
+ const yesterday = new Date(today);
17
+ yesterday.setDate(yesterday.getDate() - 1);
18
+ const msgDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
19
+
20
+ if (msgDate.getTime() === today.getTime()) {
21
+ return formatTime(date);
22
+ } else if (msgDate.getTime() === yesterday.getTime()) {
23
+ return 'Yesterday';
24
+ } else {
25
+ return formatShortDate(date);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Format time as "3:45 PM"
31
+ */
32
+ function formatTime(date) {
33
+ if (!date) return '';
34
+ const d = new Date(date);
35
+ if (isNaN(d.getTime())) return '';
36
+ let hours = d.getHours();
37
+ const minutes = d.getMinutes().toString().padStart(2, '0');
38
+ const ampm = hours >= 12 ? 'PM' : 'AM';
39
+ hours = hours % 12 || 12;
40
+ return `${hours}:${minutes} ${ampm}`;
41
+ }
42
+
43
+ /**
44
+ * Format date as "Jan 5"
45
+ */
46
+ function formatShortDate(date) {
47
+ if (!date) return '';
48
+ const d = new Date(date);
49
+ if (isNaN(d.getTime())) return '';
50
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
51
+ return `${months[d.getMonth()]} ${d.getDate()}`;
52
+ }
53
+
54
+ /**
55
+ * Format date as "January 5, 2026" for date dividers
56
+ */
57
+ function formatFullDate(date) {
58
+ if (!date) return '';
59
+ const d = new Date(date);
60
+ if (isNaN(d.getTime())) return '';
61
+ const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
62
+ return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
63
+ }
64
+
65
+ /**
66
+ * Format relative time for presence: "5 min ago", "2 hrs ago", "yesterday", "Jan 5"
67
+ */
68
+ function formatRelativeTime(timestamp) {
69
+ if (!timestamp) return 'Offline';
70
+ const date = new Date(timestamp);
71
+ if (isNaN(date.getTime())) return 'Offline';
72
+
73
+ const now = new Date();
74
+ const diffMs = now - date;
75
+ const diffSec = Math.floor(diffMs / 1000);
76
+ const diffMin = Math.floor(diffSec / 60);
77
+ const diffHr = Math.floor(diffMin / 60);
78
+ const diffDay = Math.floor(diffHr / 24);
79
+
80
+ if (diffSec < 60) return 'Just now';
81
+ if (diffMin < 60) return `${diffMin} min ago`;
82
+ if (diffHr < 24) return `${diffHr} hr${diffHr > 1 ? 's' : ''} ago`;
83
+ if (diffDay === 1) return 'yesterday';
84
+ return formatShortDate(date);
85
+ }
86
+
87
+ /**
88
+ * Get the date key for grouping messages (YYYY-MM-DD)
89
+ */
90
+ function getDateKey(timestamp) {
91
+ const d = new Date(timestamp);
92
+ if (isNaN(d.getTime())) return '';
93
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
94
+ }
95
+
96
+ /**
97
+ * Truncate text to a max length with ellipsis
98
+ */
99
+ function truncate(text, maxLen = 30) {
100
+ if (!text) return '';
101
+ if (text.length <= maxLen) return text;
102
+ return text.substring(0, maxLen) + '…';
103
+ }
104
+
105
+ /**
106
+ * Format a date for date dividers: "Today", "Yesterday", or "February 5, 2026"
107
+ */
108
+ function formatDateDivider(date) {
109
+ if (!date) return '';
110
+ const d = new Date(date);
111
+ if (isNaN(d.getTime())) return '';
112
+
113
+ const now = new Date();
114
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
115
+ const yesterday = new Date(today);
116
+ yesterday.setDate(yesterday.getDate() - 1);
117
+ const msgDate = new Date(d.getFullYear(), d.getMonth(), d.getDate());
118
+
119
+ if (msgDate.getTime() === today.getTime()) return 'Today';
120
+ if (msgDate.getTime() === yesterday.getTime()) return 'Yesterday';
121
+ return formatFullDate(d);
122
+ }
123
+
124
+ module.exports = {
125
+ formatChatListTime,
126
+ formatTime,
127
+ formatShortDate,
128
+ formatFullDate,
129
+ formatDateDivider,
130
+ formatRelativeTime,
131
+ getDateKey,
132
+ truncate
133
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * URL linkification and @mention highlighting for tacel-chat
3
+ */
4
+
5
+ const { escapeHtml } = require('./dom');
6
+
7
+ /**
8
+ * Process message text: escape HTML, convert newlines, linkify URLs, highlight @mentions
9
+ * @param {string} text - Raw message text
10
+ * @param {string} currentUsername - Current user's username (for self-mention highlighting)
11
+ * @param {boolean} enableMentions - Whether to highlight @mentions
12
+ * @param {boolean} enableLinks - Whether to linkify URLs
13
+ * @returns {string} Processed HTML string
14
+ */
15
+ function processMessageText(text, currentUsername = '', enableMentions = false, enableLinks = true) {
16
+ if (!text) return '';
17
+
18
+ let html = escapeHtml(text);
19
+
20
+ // Convert newlines to <br>
21
+ html = html.replace(/\n/g, '<br>');
22
+
23
+ // Linkify URLs
24
+ if (enableLinks) {
25
+ html = linkifyUrls(html);
26
+ }
27
+
28
+ // Highlight @mentions
29
+ if (enableMentions) {
30
+ html = highlightMentions(html, currentUsername);
31
+ }
32
+
33
+ return html;
34
+ }
35
+
36
+ /**
37
+ * Convert URLs in text to clickable links
38
+ */
39
+ function linkifyUrls(html) {
40
+ // Match http://, https://, and www. URLs
41
+ const urlRegex = /(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi;
42
+ return html.replace(urlRegex, (match) => {
43
+ const href = match.startsWith('www.') ? 'https://' + match : match;
44
+ return `<a class="tc-chat-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(match)}</a>`;
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Highlight @mentions in processed HTML
50
+ */
51
+ function highlightMentions(html, currentUsername = '') {
52
+ const mentionRegex = /@(\w+)/g;
53
+ const normalizedCurrent = (currentUsername || '').toLowerCase().trim();
54
+
55
+ return html.replace(mentionRegex, (match, username) => {
56
+ const isself = username.toLowerCase() === normalizedCurrent;
57
+ const cls = isself ? 'tc-mention tc-mention-self' : 'tc-mention';
58
+ return `<span class="${cls}">${match}</span>`;
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Extract @mention usernames from raw text
64
+ * @returns {string[]} Array of mentioned usernames (lowercase)
65
+ */
66
+ function extractMentions(text) {
67
+ if (!text) return [];
68
+ const regex = /@(\w+)/g;
69
+ const mentions = [];
70
+ let match;
71
+ while ((match = regex.exec(text)) !== null) {
72
+ const username = match[1].toLowerCase();
73
+ if (!mentions.includes(username)) {
74
+ mentions.push(username);
75
+ }
76
+ }
77
+ return mentions;
78
+ }
79
+
80
+ module.exports = { processMessageText, linkifyUrls, highlightMentions, extractMentions };