nodebb-plugin-chat-search 0.0.2 → 0.0.4
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/library.js +49 -46
- package/package.json +1 -1
- package/static/lib/main.js +196 -48
package/library.js
CHANGED
|
@@ -18,22 +18,17 @@ plugin.addClientScript = async (scripts) => {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
async function searchGlobal(socket, data) {
|
|
21
|
-
if (!socket.uid)
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
21
|
+
if (!socket.uid) throw new Error('Not logged in');
|
|
22
|
+
|
|
25
23
|
let targetUid = socket.uid;
|
|
26
24
|
if (data.targetUid && parseInt(data.targetUid, 10) !== parseInt(socket.uid, 10)) {
|
|
27
25
|
const isAdmin = await user.isAdministrator(socket.uid);
|
|
28
|
-
if (!isAdmin)
|
|
29
|
-
throw new Error('אין הרשאה.');
|
|
30
|
-
}
|
|
26
|
+
if (!isAdmin) throw new Error('אין הרשאה.');
|
|
31
27
|
targetUid = data.targetUid;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
const query = data.query;
|
|
35
31
|
const roomIds = await db.getSortedSetRevRange('uid:' + targetUid + ':chat:rooms', 0, -1);
|
|
36
|
-
|
|
37
32
|
let allResults = [];
|
|
38
33
|
|
|
39
34
|
for (const roomId of roomIds) {
|
|
@@ -41,66 +36,74 @@ async function searchGlobal(socket, data) {
|
|
|
41
36
|
if (!inRoom) continue;
|
|
42
37
|
|
|
43
38
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
39
|
+
let start = 0;
|
|
40
|
+
const batchSize = 50;
|
|
41
|
+
let roomMatches = [];
|
|
42
|
+
let continueFetching = true;
|
|
43
|
+
|
|
44
|
+
while (continueFetching) {
|
|
45
|
+
const messages = await messaging.getMessages({
|
|
46
|
+
callerUid: socket.uid,
|
|
47
|
+
uid: targetUid,
|
|
48
|
+
roomId: roomId,
|
|
49
|
+
isNew: false,
|
|
50
|
+
start: start,
|
|
51
|
+
stop: start + batchSize - 1
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
|
55
|
+
continueFetching = false;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const matches = messages.filter(msg =>
|
|
60
|
+
msg.content && msg.content.toLowerCase().includes(query.toLowerCase())
|
|
61
|
+
);
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
if (matches.length > 0) {
|
|
64
|
+
roomMatches = roomMatches.concat(matches);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (messages.length < batchSize) {
|
|
68
|
+
continueFetching = false;
|
|
69
|
+
} else {
|
|
70
|
+
start += batchSize;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (roomMatches.length > 0) {
|
|
75
|
+
const uids = await messaging.getUidsInRoom(roomId, 0, -1);
|
|
76
|
+
const usersData = await user.getUsersFields(uids, ['uid', 'username', 'picture', 'icon:text', 'icon:bgColor']);
|
|
65
77
|
const otherUsers = usersData.filter(u => parseInt(u.uid, 10) !== parseInt(targetUid, 10));
|
|
66
78
|
|
|
67
|
-
// --- לוגיקת הקיצור (2 שמות + ועוד X) ---
|
|
68
79
|
let displayName = '';
|
|
69
|
-
|
|
70
|
-
if (otherUsers.length
|
|
71
|
-
|
|
72
|
-
} else if (otherUsers.length <= 2) {
|
|
73
|
-
// אם יש 1 או 2, מציגים את כולם
|
|
74
|
-
displayName = otherUsers.map(u => u.username).join(', ');
|
|
75
|
-
} else {
|
|
76
|
-
// אם יש יותר מ-2, לוקחים את ה-2 הראשונים ומוסיפים את היתרה
|
|
80
|
+
if (otherUsers.length === 0) displayName = 'צ\'אט עצמי';
|
|
81
|
+
else if (otherUsers.length <= 2) displayName = otherUsers.map(u => u.username).join(', ');
|
|
82
|
+
else {
|
|
77
83
|
const firstTwo = otherUsers.slice(0, 2).map(u => u.username).join(', ');
|
|
78
84
|
const remaining = otherUsers.length - 2;
|
|
79
85
|
displayName = `${firstTwo} ועוד ${remaining} משתמשים`;
|
|
80
86
|
}
|
|
81
|
-
// ----------------------------------------
|
|
82
87
|
|
|
83
88
|
const roomData = await messaging.getRoomData(roomId);
|
|
84
|
-
// שם החדר: אם יש שם מוגדר לקבוצה - קח אותו, אחרת קח את השמות שיצרנו
|
|
85
89
|
let roomName = (roomData && roomData.roomName) || displayName;
|
|
86
90
|
|
|
87
|
-
|
|
91
|
+
roomMatches.forEach(m => {
|
|
88
92
|
if (!m.user || !m.user.username) {
|
|
89
93
|
const sender = usersData.find(u => parseInt(u.uid, 10) === parseInt(m.fromuid, 10));
|
|
90
|
-
m.user = sender || { username: 'Unknown' };
|
|
94
|
+
m.user = sender || { username: 'Unknown', 'icon:bgColor': '#aaa' };
|
|
91
95
|
}
|
|
92
96
|
m.roomName = roomName;
|
|
93
97
|
m.targetUid = targetUid;
|
|
94
|
-
|
|
98
|
+
m.participants = otherUsers;
|
|
95
99
|
});
|
|
96
100
|
|
|
97
|
-
allResults = allResults.concat(
|
|
101
|
+
allResults = allResults.concat(roomMatches);
|
|
98
102
|
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error(`[Chat Search] Error in room ${roomId}: ${err.message}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(`[Chat Search] Error in room ${roomId}: ${err.message}`);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
|
-
|
|
104
107
|
return allResults;
|
|
105
108
|
}
|
|
106
109
|
|
package/package.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -1,64 +1,136 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
window.chatSearchState = window.chatSearchState || {
|
|
4
|
+
query: '',
|
|
5
|
+
resultsHtml: '',
|
|
6
|
+
isOpen: false,
|
|
7
|
+
lastScroll: 0
|
|
8
|
+
};
|
|
9
|
+
|
|
3
10
|
$(document).ready(function () {
|
|
4
|
-
|
|
11
|
+
let observer = null;
|
|
5
12
|
|
|
6
13
|
$(window).on('action:ajaxify.end', function (ev, data) {
|
|
7
|
-
|
|
8
|
-
|
|
14
|
+
if (observer) observer.disconnect();
|
|
9
15
|
const isChatUrl = data.url.match(/^(user\/[^\/]+\/)?chats/);
|
|
10
|
-
const isChatTemplate = data.template && data.template.name === 'chats';
|
|
16
|
+
const isChatTemplate = data.template && (data.template.name === 'chats' || data.template === 'chats');
|
|
11
17
|
|
|
12
18
|
if (isChatUrl || isChatTemplate) {
|
|
13
|
-
|
|
19
|
+
initSearchInjection();
|
|
20
|
+
} else {
|
|
21
|
+
window.chatSearchState = { query: '', resultsHtml: '', isOpen: false, lastScroll: 0 };
|
|
14
22
|
}
|
|
15
23
|
});
|
|
16
24
|
|
|
17
25
|
$(window).on('action:chat.loaded', function (ev, data) {
|
|
26
|
+
highlightActiveChat();
|
|
18
27
|
handleScrollToMessage();
|
|
19
28
|
});
|
|
20
29
|
|
|
21
|
-
if (ajaxify.data && ajaxify.data.template
|
|
22
|
-
|
|
30
|
+
if (ajaxify.data.template && (ajaxify.data.template.name === 'chats' || ajaxify.data.template === 'chats')) {
|
|
31
|
+
initSearchInjection();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}, 200);
|
|
34
|
+
function initSearchInjection() {
|
|
35
|
+
if (!injectSearchBar()) {
|
|
36
|
+
const targetNode = document.body;
|
|
37
|
+
const config = { childList: true, subtree: true };
|
|
38
|
+
observer = new MutationObserver(function(mutationsList) {
|
|
39
|
+
const container = findContainer();
|
|
40
|
+
if (container.length > 0) {
|
|
41
|
+
injectSearchBar(container);
|
|
42
|
+
observer.disconnect();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
observer.observe(targetNode, config);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
function
|
|
42
|
-
|
|
49
|
+
function findContainer() {
|
|
50
|
+
let container = $('[component="chat/nav-wrapper"]');
|
|
51
|
+
if (container.length === 0) container = $('.chats-page').find('.col-md-4').first();
|
|
52
|
+
if (container.length === 0) container = $('[component="chat/list"]').parent();
|
|
53
|
+
return container;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function injectSearchBar(containerElement) {
|
|
57
|
+
const container = containerElement || findContainer();
|
|
58
|
+
if (container.length === 0) return false;
|
|
59
|
+
if ($('#global-chat-search-container').length > 0) return true;
|
|
43
60
|
|
|
44
61
|
const searchHtml = `
|
|
45
|
-
<div id="global-chat-search-container" style="padding: 10px; background: #fff; border-bottom: 1px solid #ddd; margin-bottom:
|
|
62
|
+
<div id="global-chat-search-container" style="padding: 10px; background: #fff; border-bottom: 1px solid #ddd; margin-bottom: 5px;">
|
|
46
63
|
<div class="input-group">
|
|
47
|
-
<input type="text" id="global-chat-search" class="form-control" placeholder="חפש הודעה..." style="font-size: 14px;">
|
|
64
|
+
<input type="text" id="global-chat-search" class="form-control" placeholder="חפש הודעה..." style="font-size: 14px; height: 34px;">
|
|
48
65
|
<span class="input-group-btn">
|
|
49
|
-
<button class="btn btn-primary" id="btn-chat-search" type="button"><i class="fa fa-search"></i></button>
|
|
66
|
+
<button class="btn btn-primary" id="btn-chat-search" type="button" style="height: 34px;"><i class="fa fa-search"></i></button>
|
|
50
67
|
</span>
|
|
51
68
|
</div>
|
|
52
|
-
<div id="global-search-results" style="margin-top: 5px; max-height: 400px;
|
|
69
|
+
<div id="global-search-results" class="chats-list overflow-auto ghost-scrollbar" style="margin-top: 5px; max-height: 400px; display:none;"></div>
|
|
53
70
|
</div>
|
|
54
71
|
`;
|
|
55
72
|
|
|
56
73
|
container.prepend(searchHtml);
|
|
57
|
-
|
|
74
|
+
restoreState();
|
|
75
|
+
attachEvents();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function attachEvents() {
|
|
58
80
|
$('#btn-chat-search').off('click').on('click', executeSearch);
|
|
59
|
-
$('#global-chat-search')
|
|
81
|
+
const input = $('#global-chat-search');
|
|
82
|
+
input.off('keypress').on('keypress', function (e) {
|
|
60
83
|
if (e.which === 13) executeSearch();
|
|
61
84
|
});
|
|
85
|
+
input.on('input', function() {
|
|
86
|
+
window.chatSearchState.query = $(this).val();
|
|
87
|
+
});
|
|
88
|
+
$('#global-search-results').on('scroll', function() {
|
|
89
|
+
window.chatSearchState.lastScroll = $(this).scrollTop();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function restoreState() {
|
|
94
|
+
const input = $('#global-chat-search');
|
|
95
|
+
const results = $('#global-search-results');
|
|
96
|
+
if (window.chatSearchState.query) input.val(window.chatSearchState.query);
|
|
97
|
+
if (window.chatSearchState.isOpen && window.chatSearchState.resultsHtml) {
|
|
98
|
+
results.html(window.chatSearchState.resultsHtml).show();
|
|
99
|
+
if ($.fn.timeago) results.find('.timeago').timeago();
|
|
100
|
+
if (window.chatSearchState.lastScroll > 0) results.scrollTop(window.chatSearchState.lastScroll);
|
|
101
|
+
highlightActiveChat();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildAvatarHtml(user, sizePx, extraStyle = '', extraClasses = '') {
|
|
106
|
+
const sizeVal = sizePx + 'px';
|
|
107
|
+
const bgStyle = `background-color: ${user['icon:bgColor'] || '#5c5c5c'};`;
|
|
108
|
+
const commonStyle = `style="--avatar-size: ${sizeVal}; width: ${sizeVal}; height: ${sizeVal}; line-height: ${sizeVal}; ${bgStyle} ${extraStyle}"`;
|
|
109
|
+
const classes = `avatar avatar-rounded ${extraClasses}`;
|
|
110
|
+
|
|
111
|
+
if (user.picture) {
|
|
112
|
+
return `<span title="${user.username}" class="${classes}" component="avatar/picture" ${commonStyle}><img src="${user.picture}" alt="${user.username}" class="avatar avatar-rounded"></span>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const text = user['icon:text'] || (user.username ? user.username[0].toUpperCase() : '?');
|
|
116
|
+
return `<span title="${user.username}" class="${classes}" component="avatar/icon" ${commonStyle}>${text}</span>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderMainAvatars(participants) {
|
|
120
|
+
if (!participants || participants.length === 0) {
|
|
121
|
+
return `<div class="main-avatar">
|
|
122
|
+
<span class="avatar avatar-rounded" style="--avatar-size: 32px; width:32px; height:32px; background-color: #ccc">?</span>
|
|
123
|
+
</div>`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `<div class="main-avatar">
|
|
127
|
+
${buildAvatarHtml(participants[0], 32)}
|
|
128
|
+
</div>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cleanContent(content) {
|
|
132
|
+
if (!content) return '';
|
|
133
|
+
return content.replace(/<\/?p[^>]*>/g, ' ').trim();
|
|
62
134
|
}
|
|
63
135
|
|
|
64
136
|
function executeSearch() {
|
|
@@ -67,43 +139,119 @@ $(document).ready(function () {
|
|
|
67
139
|
|
|
68
140
|
if (!query) {
|
|
69
141
|
resultsContainer.hide();
|
|
142
|
+
window.chatSearchState.isOpen = false;
|
|
143
|
+
window.chatSearchState.resultsHtml = '';
|
|
70
144
|
return;
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
let targetUid = ajaxify.data.uid || app.user.uid;
|
|
74
|
-
|
|
75
148
|
resultsContainer.show().html('<div class="text-center" style="padding:10px;"><i class="fa fa-spinner fa-spin"></i> מחפש...</div>');
|
|
149
|
+
window.chatSearchState.isOpen = true;
|
|
76
150
|
|
|
77
|
-
socket.emit('plugins.chatSearch.searchGlobal', {
|
|
78
|
-
query: query,
|
|
79
|
-
targetUid: targetUid
|
|
80
|
-
}, function (err, messages) {
|
|
151
|
+
socket.emit('plugins.chatSearch.searchGlobal', { query: query, targetUid: targetUid }, function (err, messages) {
|
|
81
152
|
if (err) {
|
|
82
|
-
|
|
83
|
-
resultsContainer.html('<div class="alert alert-danger" style="margin:5px;">שגיאה בחיפוש</div>');
|
|
153
|
+
resultsContainer.html('<div class="alert alert-danger" style="margin:5px;">שגיאה</div>');
|
|
84
154
|
return;
|
|
85
155
|
}
|
|
86
|
-
|
|
87
156
|
if (!messages || messages.length === 0) {
|
|
88
|
-
|
|
157
|
+
const noRes = '<div class="text-center" style="padding:10px; color:#777;">לא נמצאו תוצאות.</div>';
|
|
158
|
+
resultsContainer.html(noRes);
|
|
159
|
+
window.chatSearchState.resultsHtml = noRes;
|
|
89
160
|
return;
|
|
90
161
|
}
|
|
91
162
|
|
|
92
|
-
let html = '<
|
|
93
|
-
|
|
163
|
+
let html = '<div class="d-flex flex-column">';
|
|
94
164
|
messages.forEach(msg => {
|
|
95
|
-
const
|
|
165
|
+
const isoTime = new Date(msg.timestamp).toISOString();
|
|
96
166
|
|
|
97
|
-
let baseUrl = window.location.pathname;
|
|
167
|
+
let baseUrl = window.location.pathname.replace(/\/chats\/.*$/, '/chats');
|
|
98
168
|
if (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1);
|
|
99
|
-
|
|
100
|
-
|
|
169
|
+
|
|
101
170
|
const chatLink = baseUrl + '/' + msg.roomId + '?mid=' + msg.mid;
|
|
102
171
|
const senderName = (msg.user && msg.user.username) ? msg.user.username : 'Unknown';
|
|
103
172
|
|
|
104
|
-
|
|
173
|
+
const mainAvatarHtml = renderMainAvatars(msg.participants);
|
|
174
|
+
const senderSmallAvatar = buildAvatarHtml(msg.user, 14, 'vertical-align: text-bottom;', 'align-middle');
|
|
175
|
+
|
|
176
|
+
const cleanedContent = cleanContent(msg.content);
|
|
105
177
|
|
|
106
178
|
html += `
|
|
107
|
-
<
|
|
108
|
-
<div
|
|
109
|
-
<
|
|
179
|
+
<div component="chat/recent/room" class="rounded-1 search-result" data-roomid="${msg.roomId}">
|
|
180
|
+
<div class="d-flex gap-1 justify-content-between">
|
|
181
|
+
<a href="#" onclick="ajaxify.go('${chatLink}'); return false;" class="chat-room-btn position-relative d-flex flex-grow-1 gap-2 justify-content-start align-items-start btn btn-ghost btn-sm ff-sans text-start" style="padding: 0.5rem;">
|
|
182
|
+
|
|
183
|
+
${mainAvatarHtml}
|
|
184
|
+
|
|
185
|
+
<div class="d-flex flex-grow-1 flex-column w-100" style="min-width:0;">
|
|
186
|
+
<div component="chat/room/title" class="room-name fw-semibold text-xs text-break">
|
|
187
|
+
${msg.roomName}
|
|
188
|
+
</div>
|
|
189
|
+
<div component="chat/room/teaser">
|
|
190
|
+
|
|
191
|
+
<div class="teaser-content text-sm line-clamp-3 text-break mb-0">
|
|
192
|
+
${senderSmallAvatar}
|
|
193
|
+
<strong class="text-xs fw-semibold teaser-username">${senderName}:</strong>
|
|
194
|
+
${cleanedContent}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class="teaser-timestamp text-muted text-xs" style="margin-top: 2px; line-height: 1;">
|
|
198
|
+
<span class="timeago" title="${isoTime}"></span>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</a>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<hr class="my-1">
|
|
207
|
+
`;
|
|
208
|
+
});
|
|
209
|
+
html += '</div>';
|
|
210
|
+
|
|
211
|
+
resultsContainer.html(html);
|
|
212
|
+
|
|
213
|
+
if ($.fn.timeago) {
|
|
214
|
+
resultsContainer.find('.timeago').timeago();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
window.chatSearchState.resultsHtml = html;
|
|
218
|
+
window.chatSearchState.lastScroll = 0;
|
|
219
|
+
highlightActiveChat();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function highlightActiveChat() {
|
|
224
|
+
let currentRoomId = ajaxify.data.roomId;
|
|
225
|
+
if (!currentRoomId) {
|
|
226
|
+
const match = window.location.pathname.match(/chats\/(\d+)/);
|
|
227
|
+
if (match) currentRoomId = match[1];
|
|
228
|
+
}
|
|
229
|
+
if (!currentRoomId) return;
|
|
230
|
+
$('.search-result').removeClass('active');
|
|
231
|
+
const activeItem = $('.search-result[data-roomid="' + currentRoomId + '"]');
|
|
232
|
+
activeItem.addClass('active');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleScrollToMessage() {
|
|
236
|
+
const params = new URLSearchParams(window.location.search);
|
|
237
|
+
const mid = params.get('mid');
|
|
238
|
+
if (!mid) return;
|
|
239
|
+
scrollToId(mid);
|
|
240
|
+
let attempts = 0;
|
|
241
|
+
const scrollInt = setInterval(() => {
|
|
242
|
+
attempts++;
|
|
243
|
+
if (scrollToId(mid) || attempts > 15) clearInterval(scrollInt);
|
|
244
|
+
}, 300);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function scrollToId(mid) {
|
|
248
|
+
const el = $('[data-mid="' + mid + '"]');
|
|
249
|
+
if (el.length > 0) {
|
|
250
|
+
el[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
251
|
+
el.css('background', '#fffeca').css('transition', 'background 1s');
|
|
252
|
+
setTimeout(() => el.css('background', ''), 2000);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
});
|