@product7/feedback-sdk 1.1.7 → 1.1.9
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 +456 -0
- package/dist/README.md +456 -0
- package/dist/feedback-sdk.js +5435 -1114
- package/dist/feedback-sdk.js.map +1 -1
- package/dist/feedback-sdk.min.js +1 -1
- package/dist/feedback-sdk.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/APIService.js +272 -0
- package/src/core/FeedbackSDK.js +70 -1
- package/src/index.js +5 -1
- package/src/styles/messengerStyles.js +1657 -0
- package/src/styles/styles.js +258 -165
- package/src/widgets/BaseWidget.js +24 -21
- package/src/widgets/ButtonWidget.js +94 -53
- package/src/widgets/MessengerWidget.js +441 -0
- package/src/widgets/SurveyWidget.js +24 -8
- package/src/widgets/WidgetFactory.js +2 -0
- package/src/widgets/messenger/MessengerState.js +222 -0
- package/src/widgets/messenger/components/MessengerLauncher.js +119 -0
- package/src/widgets/messenger/components/MessengerPanel.js +130 -0
- package/src/widgets/messenger/components/NavigationTabs.js +134 -0
- package/src/widgets/messenger/views/ChangelogView.js +198 -0
- package/src/widgets/messenger/views/ChatView.js +284 -0
- package/src/widgets/messenger/views/ConversationsView.js +223 -0
- package/src/widgets/messenger/views/HelpView.js +191 -0
- package/src/widgets/messenger/views/HomeView.js +224 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChangelogView - Changelog and announcements
|
|
3
|
+
*/
|
|
4
|
+
export class ChangelogView {
|
|
5
|
+
constructor(state, options = {}) {
|
|
6
|
+
this.state = state;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.element = null;
|
|
9
|
+
this._unsubscribe = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
this.element = document.createElement('div');
|
|
14
|
+
this.element.className = 'messenger-view messenger-changelog-view';
|
|
15
|
+
|
|
16
|
+
this._updateContent();
|
|
17
|
+
|
|
18
|
+
// Subscribe to state changes
|
|
19
|
+
this._unsubscribe = this.state.subscribe((type) => {
|
|
20
|
+
if (type === 'changelogUpdate') {
|
|
21
|
+
this._updateChangelogList();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return this.element;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_updateContent() {
|
|
29
|
+
const avatarsHtml = this._renderTeamAvatars();
|
|
30
|
+
|
|
31
|
+
this.element.innerHTML = `
|
|
32
|
+
<div class="messenger-changelog-header">
|
|
33
|
+
<h2>Changelog</h2>
|
|
34
|
+
<button class="messenger-close-btn" aria-label="Close">
|
|
35
|
+
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="messenger-changelog-subheader">
|
|
40
|
+
<span class="messenger-changelog-latest">Latest</span>
|
|
41
|
+
<div class="messenger-changelog-team">
|
|
42
|
+
<span>From ${this.state.teamName}</span>
|
|
43
|
+
${avatarsHtml}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="messenger-changelog-body">
|
|
48
|
+
<div class="messenger-changelog-list"></div>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
this._updateChangelogList();
|
|
53
|
+
this._attachEvents();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_updateChangelogList() {
|
|
57
|
+
const changelogContainer = this.element.querySelector(
|
|
58
|
+
'.messenger-changelog-list'
|
|
59
|
+
);
|
|
60
|
+
const changelogItems = this.state.changelogItems;
|
|
61
|
+
|
|
62
|
+
if (changelogItems.length === 0) {
|
|
63
|
+
changelogContainer.innerHTML = this._renderEmptyState();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
changelogContainer.innerHTML = changelogItems
|
|
68
|
+
.map((item) => this._renderChangelogCard(item))
|
|
69
|
+
.join('');
|
|
70
|
+
|
|
71
|
+
// Attach click events
|
|
72
|
+
this._attachChangelogEvents();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_renderChangelogCard(item) {
|
|
76
|
+
const tagsHtml =
|
|
77
|
+
item.tags && item.tags.length > 0
|
|
78
|
+
? `<div class="messenger-changelog-tags">
|
|
79
|
+
${item.tags.map((tag) => `<span class="messenger-changelog-tag">${tag}</span>`).join('')}
|
|
80
|
+
</div>`
|
|
81
|
+
: '';
|
|
82
|
+
|
|
83
|
+
const dateStr = this._formatDate(item.publishedAt || item.date);
|
|
84
|
+
|
|
85
|
+
return `
|
|
86
|
+
<div class="messenger-changelog-card" data-changelog-id="${item.id}">
|
|
87
|
+
${
|
|
88
|
+
item.coverImage
|
|
89
|
+
? `
|
|
90
|
+
<div class="messenger-changelog-cover">
|
|
91
|
+
<img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
|
|
92
|
+
</div>
|
|
93
|
+
`
|
|
94
|
+
: ''
|
|
95
|
+
}
|
|
96
|
+
<div class="messenger-changelog-content">
|
|
97
|
+
${tagsHtml}
|
|
98
|
+
<h3 class="messenger-changelog-title">${item.title}</h3>
|
|
99
|
+
${item.description ? `<p class="messenger-changelog-description">${this._truncateText(item.description, 100)}</p>` : ''}
|
|
100
|
+
<div class="messenger-changelog-meta">
|
|
101
|
+
<span class="messenger-changelog-date">${dateStr}</span>
|
|
102
|
+
<i class="ph ph-caret-right messenger-changelog-arrow" style="font-size: 16px;"></i>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
_renderTeamAvatars() {
|
|
110
|
+
const avatars = this.state.teamAvatars;
|
|
111
|
+
if (!avatars || avatars.length === 0) {
|
|
112
|
+
return `
|
|
113
|
+
<div class="messenger-avatar-stack messenger-avatar-stack-tiny">
|
|
114
|
+
<div class="messenger-avatar messenger-avatar-tiny" style="background: #5856d6;">S</div>
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const colors = ['#5856d6', '#007aff', '#34c759'];
|
|
120
|
+
const avatarItems = avatars
|
|
121
|
+
.slice(0, 2)
|
|
122
|
+
.map((avatar, i) => {
|
|
123
|
+
if (typeof avatar === 'string' && avatar.startsWith('http')) {
|
|
124
|
+
return `<img class="messenger-avatar messenger-avatar-tiny" src="${avatar}" alt="Team member" style="z-index: ${2 - i};" />`;
|
|
125
|
+
}
|
|
126
|
+
return `<div class="messenger-avatar messenger-avatar-tiny" style="background: ${colors[i % colors.length]}; z-index: ${2 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
|
|
127
|
+
})
|
|
128
|
+
.join('');
|
|
129
|
+
|
|
130
|
+
return `<div class="messenger-avatar-stack messenger-avatar-stack-tiny">${avatarItems}</div>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_renderEmptyState() {
|
|
134
|
+
return `
|
|
135
|
+
<div class="messenger-changelog-empty">
|
|
136
|
+
<div class="messenger-changelog-empty-icon">
|
|
137
|
+
<i class="ph ph-megaphone" style="font-size: 48px;"></i>
|
|
138
|
+
</div>
|
|
139
|
+
<h3>No changelog yet</h3>
|
|
140
|
+
<p>Check back later for updates</p>
|
|
141
|
+
</div>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_formatDate(dateString) {
|
|
146
|
+
if (!dateString) return '';
|
|
147
|
+
const date = new Date(dateString);
|
|
148
|
+
return date.toLocaleDateString('en-US', {
|
|
149
|
+
month: 'short',
|
|
150
|
+
day: 'numeric',
|
|
151
|
+
year: 'numeric',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_truncateText(text, maxLength) {
|
|
156
|
+
if (!text) return '';
|
|
157
|
+
if (text.length <= maxLength) return text;
|
|
158
|
+
return text.substring(0, maxLength).trim() + '...';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_attachEvents() {
|
|
162
|
+
// Close button
|
|
163
|
+
this.element
|
|
164
|
+
.querySelector('.messenger-close-btn')
|
|
165
|
+
.addEventListener('click', () => {
|
|
166
|
+
this.state.setOpen(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this._attachChangelogEvents();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_attachChangelogEvents() {
|
|
173
|
+
this.element
|
|
174
|
+
.querySelectorAll('.messenger-changelog-card')
|
|
175
|
+
.forEach((card) => {
|
|
176
|
+
card.addEventListener('click', () => {
|
|
177
|
+
const changelogId = card.dataset.changelogId;
|
|
178
|
+
const changelogItem = this.state.changelogItems.find(
|
|
179
|
+
(n) => n.id === changelogId
|
|
180
|
+
);
|
|
181
|
+
if (changelogItem && changelogItem.url) {
|
|
182
|
+
window.open(changelogItem.url, '_blank');
|
|
183
|
+
} else if (this.options.onChangelogClick) {
|
|
184
|
+
this.options.onChangelogClick(changelogItem);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
destroy() {
|
|
191
|
+
if (this._unsubscribe) {
|
|
192
|
+
this._unsubscribe();
|
|
193
|
+
}
|
|
194
|
+
if (this.element && this.element.parentNode) {
|
|
195
|
+
this.element.parentNode.removeChild(this.element);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatView - Individual conversation chat
|
|
3
|
+
*/
|
|
4
|
+
export class ChatView {
|
|
5
|
+
constructor(state, options = {}) {
|
|
6
|
+
this.state = state;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.element = null;
|
|
9
|
+
this._unsubscribe = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
this.element = document.createElement('div');
|
|
14
|
+
this.element.className = 'messenger-view messenger-chat-view';
|
|
15
|
+
|
|
16
|
+
this._updateContent();
|
|
17
|
+
|
|
18
|
+
// Subscribe to state changes
|
|
19
|
+
this._unsubscribe = this.state.subscribe((type, data) => {
|
|
20
|
+
if (
|
|
21
|
+
type === 'messageAdded' &&
|
|
22
|
+
data.conversationId === this.state.activeConversationId
|
|
23
|
+
) {
|
|
24
|
+
this._appendMessage(data.message);
|
|
25
|
+
this._scrollToBottom();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return this.element;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_updateContent() {
|
|
33
|
+
const conversation = this.state.getActiveConversation();
|
|
34
|
+
const messages = this.state.getActiveMessages();
|
|
35
|
+
|
|
36
|
+
const messagesHtml =
|
|
37
|
+
messages.length === 0
|
|
38
|
+
? this._renderEmptyState()
|
|
39
|
+
: messages.map((msg) => this._renderMessage(msg)).join('');
|
|
40
|
+
|
|
41
|
+
const avatarHtml = this._renderConversationAvatar(conversation);
|
|
42
|
+
|
|
43
|
+
this.element.innerHTML = `
|
|
44
|
+
<div class="messenger-chat-header">
|
|
45
|
+
<button class="messenger-back-btn" aria-label="Back">
|
|
46
|
+
<i class="ph ph-caret-left" style="font-size: 20px;"></i>
|
|
47
|
+
</button>
|
|
48
|
+
<div class="messenger-chat-header-info">
|
|
49
|
+
${avatarHtml}
|
|
50
|
+
<span class="messenger-chat-title">${conversation?.title || 'Chat with team'}</span>
|
|
51
|
+
</div>
|
|
52
|
+
<button class="messenger-close-btn" aria-label="Close">
|
|
53
|
+
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="messenger-chat-messages">
|
|
58
|
+
${messagesHtml}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="messenger-chat-compose">
|
|
62
|
+
<div class="messenger-compose-input-wrapper">
|
|
63
|
+
<textarea class="messenger-compose-input" placeholder="Write a message..." rows="1"></textarea>
|
|
64
|
+
</div>
|
|
65
|
+
<button class="messenger-compose-send" aria-label="Send" disabled>
|
|
66
|
+
<i class="ph ph-paper-plane-tilt" style="font-size: 20px;"></i>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
this._attachEvents();
|
|
72
|
+
this._scrollToBottom();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_renderEmptyState() {
|
|
76
|
+
const avatarHtml = this._renderTeamAvatars();
|
|
77
|
+
return `
|
|
78
|
+
<div class="messenger-chat-empty">
|
|
79
|
+
<div class="messenger-chat-empty-avatars">${avatarHtml}</div>
|
|
80
|
+
<h3>Start the conversation</h3>
|
|
81
|
+
<p>Send us a message and we'll get back to you as soon as possible.</p>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_renderMessage(message) {
|
|
87
|
+
const isOwn = message.isOwn;
|
|
88
|
+
const messageClass = isOwn
|
|
89
|
+
? 'messenger-message-own'
|
|
90
|
+
: 'messenger-message-received';
|
|
91
|
+
const timeStr = this._formatMessageTime(message.timestamp);
|
|
92
|
+
|
|
93
|
+
if (isOwn) {
|
|
94
|
+
return `
|
|
95
|
+
<div class="messenger-message ${messageClass}">
|
|
96
|
+
<div class="messenger-message-bubble">
|
|
97
|
+
<div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="messenger-message-time">${timeStr}</div>
|
|
100
|
+
</div>
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const avatarHtml = this._renderSenderAvatar(message.sender);
|
|
105
|
+
return `
|
|
106
|
+
<div class="messenger-message ${messageClass}">
|
|
107
|
+
<div class="messenger-message-avatar">${avatarHtml}</div>
|
|
108
|
+
<div class="messenger-message-wrapper">
|
|
109
|
+
<div class="messenger-message-sender">${message.sender?.name || 'Support'}</div>
|
|
110
|
+
<div class="messenger-message-bubble">
|
|
111
|
+
<div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="messenger-message-time">${timeStr}</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_renderSenderAvatar(sender) {
|
|
120
|
+
if (sender?.avatarUrl) {
|
|
121
|
+
return `<img class="messenger-avatar messenger-avatar-small" src="${sender.avatarUrl}" alt="${sender.name}" />`;
|
|
122
|
+
}
|
|
123
|
+
const initial = (sender?.name || 'S').charAt(0).toUpperCase();
|
|
124
|
+
return `<div class="messenger-avatar messenger-avatar-small" style="background: #5856d6;">${initial}</div>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_renderConversationAvatar(conversation) {
|
|
128
|
+
if (!conversation?.participants?.length) {
|
|
129
|
+
return `<div class="messenger-avatar messenger-avatar-small" style="background: #5856d6;">S</div>`;
|
|
130
|
+
}
|
|
131
|
+
const p = conversation.participants[0];
|
|
132
|
+
if (p.avatarUrl) {
|
|
133
|
+
return `<img class="messenger-avatar messenger-avatar-small" src="${p.avatarUrl}" alt="${p.name}" />`;
|
|
134
|
+
}
|
|
135
|
+
return `<div class="messenger-avatar messenger-avatar-small" style="background: #5856d6;">${(p.name || 'S').charAt(0).toUpperCase()}</div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_renderTeamAvatars() {
|
|
139
|
+
const avatars = this.state.teamAvatars;
|
|
140
|
+
if (!avatars || avatars.length === 0) {
|
|
141
|
+
return `
|
|
142
|
+
<div class="messenger-avatar-stack">
|
|
143
|
+
<div class="messenger-avatar" style="background: #5856d6;">S</div>
|
|
144
|
+
<div class="messenger-avatar" style="background: #007aff;">T</div>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
|
|
150
|
+
const avatarItems = avatars
|
|
151
|
+
.slice(0, 3)
|
|
152
|
+
.map((avatar, i) => {
|
|
153
|
+
if (typeof avatar === 'string' && avatar.startsWith('http')) {
|
|
154
|
+
return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
|
|
155
|
+
}
|
|
156
|
+
return `<div class="messenger-avatar" style="background: ${colors[i % colors.length]}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
|
|
157
|
+
})
|
|
158
|
+
.join('');
|
|
159
|
+
|
|
160
|
+
return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_formatMessageTime(timestamp) {
|
|
164
|
+
if (!timestamp) return '';
|
|
165
|
+
const date = new Date(timestamp);
|
|
166
|
+
return date.toLocaleTimeString('en-US', {
|
|
167
|
+
hour: 'numeric',
|
|
168
|
+
minute: '2-digit',
|
|
169
|
+
hour12: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
_formatMessageContent(content) {
|
|
174
|
+
if (!content) return '';
|
|
175
|
+
// Basic HTML escaping and line breaks
|
|
176
|
+
return content
|
|
177
|
+
.replace(/&/g, '&')
|
|
178
|
+
.replace(/</g, '<')
|
|
179
|
+
.replace(/>/g, '>')
|
|
180
|
+
.replace(/\n/g, '<br>');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_appendMessage(message) {
|
|
184
|
+
const messagesContainer = this.element.querySelector(
|
|
185
|
+
'.messenger-chat-messages'
|
|
186
|
+
);
|
|
187
|
+
const emptyState = messagesContainer.querySelector('.messenger-chat-empty');
|
|
188
|
+
if (emptyState) {
|
|
189
|
+
emptyState.remove();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const messageHtml = this._renderMessage(message);
|
|
193
|
+
const tempDiv = document.createElement('div');
|
|
194
|
+
tempDiv.innerHTML = messageHtml;
|
|
195
|
+
messagesContainer.appendChild(tempDiv.firstElementChild);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_scrollToBottom() {
|
|
199
|
+
const messagesContainer = this.element.querySelector(
|
|
200
|
+
'.messenger-chat-messages'
|
|
201
|
+
);
|
|
202
|
+
if (messagesContainer) {
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
205
|
+
}, 50);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_attachEvents() {
|
|
210
|
+
// Back button
|
|
211
|
+
this.element
|
|
212
|
+
.querySelector('.messenger-back-btn')
|
|
213
|
+
.addEventListener('click', () => {
|
|
214
|
+
this.state.setView('messages');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Close button
|
|
218
|
+
this.element
|
|
219
|
+
.querySelector('.messenger-close-btn')
|
|
220
|
+
.addEventListener('click', () => {
|
|
221
|
+
this.state.setOpen(false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Compose input
|
|
225
|
+
const input = this.element.querySelector('.messenger-compose-input');
|
|
226
|
+
const sendBtn = this.element.querySelector('.messenger-compose-send');
|
|
227
|
+
|
|
228
|
+
input.addEventListener('input', () => {
|
|
229
|
+
// Auto-resize textarea
|
|
230
|
+
input.style.height = 'auto';
|
|
231
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
232
|
+
|
|
233
|
+
// Enable/disable send button
|
|
234
|
+
sendBtn.disabled = !input.value.trim();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
input.addEventListener('keydown', (e) => {
|
|
238
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
this._sendMessage();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
sendBtn.addEventListener('click', () => {
|
|
245
|
+
this._sendMessage();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_sendMessage() {
|
|
250
|
+
const input = this.element.querySelector('.messenger-compose-input');
|
|
251
|
+
const content = input.value.trim();
|
|
252
|
+
|
|
253
|
+
if (!content) return;
|
|
254
|
+
|
|
255
|
+
// Add message to state
|
|
256
|
+
const message = {
|
|
257
|
+
id: 'msg_' + Date.now(),
|
|
258
|
+
content: content,
|
|
259
|
+
isOwn: true,
|
|
260
|
+
timestamp: new Date().toISOString(),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
this.state.addMessage(this.state.activeConversationId, message);
|
|
264
|
+
|
|
265
|
+
// Clear input
|
|
266
|
+
input.value = '';
|
|
267
|
+
input.style.height = 'auto';
|
|
268
|
+
this.element.querySelector('.messenger-compose-send').disabled = true;
|
|
269
|
+
|
|
270
|
+
// Emit event for API integration
|
|
271
|
+
if (this.options.onSendMessage) {
|
|
272
|
+
this.options.onSendMessage(this.state.activeConversationId, message);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
destroy() {
|
|
277
|
+
if (this._unsubscribe) {
|
|
278
|
+
this._unsubscribe();
|
|
279
|
+
}
|
|
280
|
+
if (this.element && this.element.parentNode) {
|
|
281
|
+
this.element.parentNode.removeChild(this.element);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConversationsView - Message thread list
|
|
3
|
+
*/
|
|
4
|
+
export class ConversationsView {
|
|
5
|
+
constructor(state, options = {}) {
|
|
6
|
+
this.state = state;
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.element = null;
|
|
9
|
+
this._unsubscribe = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
render() {
|
|
13
|
+
this.element = document.createElement('div');
|
|
14
|
+
this.element.className = 'messenger-view messenger-conversations-view';
|
|
15
|
+
|
|
16
|
+
this._updateContent();
|
|
17
|
+
this._attachEvents();
|
|
18
|
+
|
|
19
|
+
// Subscribe to state changes
|
|
20
|
+
this._unsubscribe = this.state.subscribe((type) => {
|
|
21
|
+
if (
|
|
22
|
+
type === 'conversationsUpdate' ||
|
|
23
|
+
type === 'conversationAdded' ||
|
|
24
|
+
type === 'conversationRead'
|
|
25
|
+
) {
|
|
26
|
+
this._updateContent();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return this.element;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_updateContent() {
|
|
34
|
+
const conversations = this.state.conversations;
|
|
35
|
+
const avatarsHtml = this._renderAvatarStack();
|
|
36
|
+
|
|
37
|
+
let conversationsHtml;
|
|
38
|
+
if (conversations.length === 0) {
|
|
39
|
+
conversationsHtml = `
|
|
40
|
+
<div class="messenger-conversations-empty">
|
|
41
|
+
<div class="messenger-conversations-empty-icon">
|
|
42
|
+
<i class="ph ph-chat" style="font-size: 48px;"></i>
|
|
43
|
+
</div>
|
|
44
|
+
<h3>No conversations yet</h3>
|
|
45
|
+
<p>Start a new conversation with our team</p>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
} else {
|
|
49
|
+
conversationsHtml = `
|
|
50
|
+
<div class="messenger-conversations-list">
|
|
51
|
+
${conversations.map((conv) => this._renderConversationItem(conv)).join('')}
|
|
52
|
+
</div>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.element.innerHTML = `
|
|
57
|
+
<div class="messenger-conversations-header">
|
|
58
|
+
<h2>Messages</h2>
|
|
59
|
+
<button class="messenger-close-btn" aria-label="Close">
|
|
60
|
+
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="messenger-conversations-body">
|
|
65
|
+
${conversationsHtml}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="messenger-conversations-footer">
|
|
69
|
+
<button class="messenger-new-message-btn">
|
|
70
|
+
<div class="messenger-new-message-avatars">${avatarsHtml}</div>
|
|
71
|
+
<span>Send us a message</span>
|
|
72
|
+
<i class="ph ph-arrow-right" style="font-size: 16px;"></i>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
this._attachEvents();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_renderConversationItem(conversation) {
|
|
81
|
+
const unreadClass = conversation.unread > 0 ? 'unread' : '';
|
|
82
|
+
const timeAgo = this._formatTimeAgo(conversation.lastMessageTime);
|
|
83
|
+
const avatarsHtml = this._renderConversationAvatars(
|
|
84
|
+
conversation.participants
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return `
|
|
88
|
+
<div class="messenger-conversation-item ${unreadClass}" data-conversation-id="${conversation.id}">
|
|
89
|
+
<div class="messenger-conversation-avatars">
|
|
90
|
+
${avatarsHtml}
|
|
91
|
+
</div>
|
|
92
|
+
<div class="messenger-conversation-content">
|
|
93
|
+
<div class="messenger-conversation-header">
|
|
94
|
+
<span class="messenger-conversation-title">${conversation.title || 'Chat with team'}</span>
|
|
95
|
+
<span class="messenger-conversation-time">${timeAgo}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="messenger-conversation-preview">
|
|
98
|
+
${conversation.unread > 0 ? '<span class="messenger-unread-dot"></span>' : ''}
|
|
99
|
+
<span class="messenger-conversation-message">${this._truncateMessage(conversation.lastMessage)}</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_renderConversationAvatars(participants) {
|
|
107
|
+
if (!participants || participants.length === 0) {
|
|
108
|
+
return `<div class="messenger-avatar messenger-avatar-medium" style="background: #5856d6;">S</div>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const p = participants[0];
|
|
112
|
+
if (p.avatarUrl) {
|
|
113
|
+
return `<img class="messenger-avatar messenger-avatar-medium" src="${p.avatarUrl}" alt="${p.name}" />`;
|
|
114
|
+
}
|
|
115
|
+
return `<div class="messenger-avatar messenger-avatar-medium" style="background: ${this._getAvatarColor(0)};">${(p.name || 'S').charAt(0).toUpperCase()}</div>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_renderAvatarStack() {
|
|
119
|
+
const avatars = this.state.teamAvatars;
|
|
120
|
+
if (!avatars || avatars.length === 0) {
|
|
121
|
+
return `
|
|
122
|
+
<div class="messenger-avatar-stack messenger-avatar-stack-small">
|
|
123
|
+
<div class="messenger-avatar messenger-avatar-small" style="background: #5856d6;">S</div>
|
|
124
|
+
<div class="messenger-avatar messenger-avatar-small" style="background: #007aff;">T</div>
|
|
125
|
+
</div>
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const avatarItems = avatars
|
|
130
|
+
.slice(0, 2)
|
|
131
|
+
.map((avatar, i) => {
|
|
132
|
+
if (typeof avatar === 'string' && avatar.startsWith('http')) {
|
|
133
|
+
return `<img class="messenger-avatar messenger-avatar-small" src="${avatar}" alt="Team member" style="z-index: ${2 - i};" />`;
|
|
134
|
+
}
|
|
135
|
+
return `<div class="messenger-avatar messenger-avatar-small" style="background: ${this._getAvatarColor(i)}; z-index: ${2 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
|
|
136
|
+
})
|
|
137
|
+
.join('');
|
|
138
|
+
|
|
139
|
+
return `<div class="messenger-avatar-stack messenger-avatar-stack-small">${avatarItems}</div>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_getAvatarColor(index) {
|
|
143
|
+
const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
|
|
144
|
+
return colors[index % colors.length];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_formatTimeAgo(timestamp) {
|
|
148
|
+
if (!timestamp) return '';
|
|
149
|
+
const date = new Date(timestamp);
|
|
150
|
+
const now = new Date();
|
|
151
|
+
const diffMs = now - date;
|
|
152
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
153
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
154
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
155
|
+
|
|
156
|
+
if (diffMins < 1) return 'now';
|
|
157
|
+
if (diffMins < 60) return `${diffMins}m`;
|
|
158
|
+
if (diffHours < 24) return `${diffHours}h`;
|
|
159
|
+
if (diffDays < 7) return `${diffDays}d`;
|
|
160
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_truncateMessage(message) {
|
|
164
|
+
if (!message) return 'No messages yet';
|
|
165
|
+
const maxLength = 50;
|
|
166
|
+
if (message.length <= maxLength) return message;
|
|
167
|
+
return message.substring(0, maxLength) + '...';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_attachEvents() {
|
|
171
|
+
// Close button
|
|
172
|
+
const closeBtn = this.element.querySelector('.messenger-close-btn');
|
|
173
|
+
if (closeBtn) {
|
|
174
|
+
closeBtn.addEventListener('click', () => {
|
|
175
|
+
this.state.setOpen(false);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Conversation items
|
|
180
|
+
this.element
|
|
181
|
+
.querySelectorAll('.messenger-conversation-item')
|
|
182
|
+
.forEach((item) => {
|
|
183
|
+
item.addEventListener('click', () => {
|
|
184
|
+
const convId = item.dataset.conversationId;
|
|
185
|
+
this.state.setActiveConversation(convId);
|
|
186
|
+
this.state.markAsRead(convId);
|
|
187
|
+
this.state.setView('chat');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// New message button
|
|
192
|
+
const newMsgBtn = this.element.querySelector('.messenger-new-message-btn');
|
|
193
|
+
if (newMsgBtn) {
|
|
194
|
+
newMsgBtn.addEventListener('click', () => {
|
|
195
|
+
this._startNewConversation();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_startNewConversation() {
|
|
201
|
+
// Create a new conversation and navigate to chat
|
|
202
|
+
const newConv = {
|
|
203
|
+
id: 'conv_' + Date.now(),
|
|
204
|
+
title: 'New conversation',
|
|
205
|
+
participants: [],
|
|
206
|
+
lastMessage: null,
|
|
207
|
+
lastMessageTime: new Date().toISOString(),
|
|
208
|
+
unread: 0,
|
|
209
|
+
};
|
|
210
|
+
this.state.addConversation(newConv);
|
|
211
|
+
this.state.setActiveConversation(newConv.id);
|
|
212
|
+
this.state.setView('chat');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
destroy() {
|
|
216
|
+
if (this._unsubscribe) {
|
|
217
|
+
this._unsubscribe();
|
|
218
|
+
}
|
|
219
|
+
if (this.element && this.element.parentNode) {
|
|
220
|
+
this.element.parentNode.removeChild(this.element);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|