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.
- package/README.md +1435 -0
- package/chat-api.js +436 -0
- package/chat-sync.js +65 -0
- package/chat.css +2573 -0
- package/chat.js +834 -0
- package/components/attachment.js +400 -0
- package/components/confirm.js +125 -0
- package/components/context-menu.js +461 -0
- package/components/files-panel.js +228 -0
- package/components/mention.js +198 -0
- package/components/message-area.js +612 -0
- package/components/message.js +200 -0
- package/components/new-chat.js +130 -0
- package/components/pinned-panel.js +184 -0
- package/components/presence.js +45 -0
- package/components/search-panel.js +201 -0
- package/components/sidebar.js +278 -0
- package/components/tabs.js +130 -0
- package/index.js +12 -0
- package/package.json +41 -0
- package/themes.js +495 -0
- package/utils/dom.js +75 -0
- package/utils/format.js +133 -0
- package/utils/linkify.js +80 -0
package/utils/format.js
ADDED
|
@@ -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
|
+
};
|
package/utils/linkify.js
ADDED
|
@@ -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 };
|