nodebb-plugin-chat-search 0.0.3 → 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.
Files changed (3) hide show
  1. package/library.js +49 -46
  2. package/package.json +1 -1
  3. package/static/lib/main.js +119 -117
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
- throw new Error('Not logged in');
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
- const messages = await messaging.getMessages({
45
- callerUid: socket.uid,
46
- uid: targetUid,
47
- roomId: roomId,
48
- isNew: false,
49
- start: 0,
50
- stop: -1
51
- });
52
-
53
- if (!messages || !Array.isArray(messages)) continue;
54
-
55
- const matches = messages.filter(msg =>
56
- msg.content && msg.content.toLowerCase().includes(query.toLowerCase())
57
- );
58
-
59
- if (matches.length > 0) {
60
- // שליפת משתמשים
61
- const uids = await messaging.getUidsInRoom(roomId, 0, -1);
62
- const usersData = await user.getUsersFields(uids, ['uid', 'username']);
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 === 0) {
71
- displayName = 'צ\'אט עצמי'; // או משתמש מחוק
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
- matches.forEach(m => {
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
- // מחקנו את chatWith כי כבר לא צריך אותו
98
+ m.participants = otherUsers;
95
99
  });
96
100
 
97
- allResults = allResults.concat(matches);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-chat-search",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A plugin to search text within NodeBB chats",
5
5
  "main": "library.js",
6
6
  "nbbpm": {
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // מצב גלובלי לזיכרון בין מעברי דפים
4
3
  window.chatSearchState = window.chatSearchState || {
5
4
  query: '',
6
5
  resultsHtml: '',
@@ -9,27 +8,16 @@ window.chatSearchState = window.chatSearchState || {
9
8
  };
10
9
 
11
10
  $(document).ready(function () {
12
- console.log('[Chat Search] Loaded (Instant No-Flicker).');
13
-
14
11
  let observer = null;
15
12
 
16
- // האזנה למעבר דפים
17
- $(window).on('action:ajaxify.start', function () {
18
- // לפני שהדף מתחלף, ננסה לשמור את האלמנט בזיכרון אם נרצה להחזירו (אופציונלי)
19
- // כרגע אנו מסתמכים על בנייה מחדש מהירה מאוד
20
- });
21
-
22
13
  $(window).on('action:ajaxify.end', function (ev, data) {
23
- // אם יש observer ישן, ננתק אותו כדי לא להעמיס
24
14
  if (observer) observer.disconnect();
25
-
26
15
  const isChatUrl = data.url.match(/^(user\/[^\/]+\/)?chats/);
27
- const isChatTemplate = data.template && data.template.name === 'chats';
16
+ const isChatTemplate = data.template && (data.template.name === 'chats' || data.template === 'chats');
28
17
 
29
18
  if (isChatUrl || isChatTemplate) {
30
- initFastInjection();
19
+ initSearchInjection();
31
20
  } else {
32
- // יציאה מאזור הצ'אטים - איפוס הזיכרון
33
21
  window.chatSearchState = { query: '', resultsHtml: '', isOpen: false, lastScroll: 0 };
34
22
  }
35
23
  });
@@ -39,113 +27,110 @@ $(document).ready(function () {
39
27
  handleScrollToMessage();
40
28
  });
41
29
 
42
- // בדיקה ראשונית
43
- if (ajaxify.data && ajaxify.data.template && ajaxify.data.template.name === 'chats') {
44
- initFastInjection();
30
+ if (ajaxify.data.template && (ajaxify.data.template.name === 'chats' || ajaxify.data.template === 'chats')) {
31
+ initSearchInjection();
45
32
  }
46
33
 
47
- function initFastInjection() {
48
- // 1. ניסיון הזרקה מיידי
49
- tryInject();
50
-
51
- // 2. הפעלת MutationObserver לזיהוי שינויים ב-DOM בזמן אמת (מונע קפיצות)
52
- const targetNode = document.body;
53
- const config = { childList: true, subtree: true };
54
-
55
- observer = new MutationObserver(function(mutationsList) {
56
- for(let mutation of mutationsList) {
57
- if (mutation.type === 'childList') {
58
- // אם נוספו אלמנטים לדף, נבדוק אם הסרגל הגיע
59
- const container = findContainer();
60
- if (container.length > 0 && container.find('#global-chat-search-container').length === 0) {
61
- addGlobalSearchBar(container);
62
- }
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();
63
43
  }
64
- }
65
- });
66
-
67
- observer.observe(targetNode, config);
68
-
69
- // 3. גיבוי: Interval מהיר מאוד (50ms) למקרה שה-Observer פספס
70
- let attempts = 0;
71
- const interval = setInterval(() => {
72
- attempts++;
73
- if (tryInject()) {
74
- // לא עוצרים את האינטרוול מיד, כי לפעמים NodeBB מרענן פעמיים
75
- if (attempts > 20) clearInterval(interval);
76
- } else if (attempts > 40) { // 2 שניות
77
- clearInterval(interval);
78
- }
79
- }, 50);
44
+ });
45
+ observer.observe(targetNode, config);
46
+ }
80
47
  }
81
48
 
82
49
  function findContainer() {
83
- // סדר עדיפויות למציאת הקונטיינר
84
50
  let container = $('[component="chat/nav-wrapper"]');
85
51
  if (container.length === 0) container = $('.chats-page').find('.col-md-4').first();
52
+ if (container.length === 0) container = $('[component="chat/list"]').parent();
86
53
  return container;
87
54
  }
88
55
 
89
- function tryInject() {
90
- const container = findContainer();
91
- if (container.length > 0) {
92
- addGlobalSearchBar(container);
93
- return true;
94
- }
95
- return false;
96
- }
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;
97
60
 
98
- function addGlobalSearchBar(container) {
99
- if ($('#global-chat-search-container').length > 0) return;
100
-
101
- // ה-HTML של התיבה
102
61
  const searchHtml = `
103
- <div id="global-chat-search-container" style="padding: 10px; background: #fff; border-bottom: 1px solid #ddd; margin-bottom: 10px;">
62
+ <div id="global-chat-search-container" style="padding: 10px; background: #fff; border-bottom: 1px solid #ddd; margin-bottom: 5px;">
104
63
  <div class="input-group">
105
64
  <input type="text" id="global-chat-search" class="form-control" placeholder="חפש הודעה..." style="font-size: 14px; height: 34px;">
106
65
  <span class="input-group-btn">
107
66
  <button class="btn btn-primary" id="btn-chat-search" type="button" style="height: 34px;"><i class="fa fa-search"></i></button>
108
67
  </span>
109
68
  </div>
110
- <div id="global-search-results" style="margin-top: 5px; max-height: 400px; overflow-y: auto; background: white; border: 1px solid #eee; display:none;"></div>
69
+ <div id="global-search-results" class="chats-list overflow-auto ghost-scrollbar" style="margin-top: 5px; max-height: 400px; display:none;"></div>
111
70
  </div>
112
71
  `;
113
72
 
114
73
  container.prepend(searchHtml);
115
-
116
- // --- שחזור מיידי של המצב (מונע הבהוב של התוכן) ---
74
+ restoreState();
75
+ attachEvents();
76
+ return true;
77
+ }
78
+
79
+ function attachEvents() {
80
+ $('#btn-chat-search').off('click').on('click', executeSearch);
81
+ const input = $('#global-chat-search');
82
+ input.off('keypress').on('keypress', function (e) {
83
+ if (e.which === 13) executeSearch();
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() {
117
94
  const input = $('#global-chat-search');
118
95
  const results = $('#global-search-results');
119
-
120
- if (window.chatSearchState.query) {
121
- input.val(window.chatSearchState.query);
122
- }
123
-
96
+ if (window.chatSearchState.query) input.val(window.chatSearchState.query);
124
97
  if (window.chatSearchState.isOpen && window.chatSearchState.resultsHtml) {
125
98
  results.html(window.chatSearchState.resultsHtml).show();
126
- // שחזור מיקום הגלילה
127
- if (window.chatSearchState.lastScroll > 0) {
128
- results.scrollTop(window.chatSearchState.lastScroll);
129
- }
99
+ if ($.fn.timeago) results.find('.timeago').timeago();
100
+ if (window.chatSearchState.lastScroll > 0) results.scrollTop(window.chatSearchState.lastScroll);
130
101
  highlightActiveChat();
131
102
  }
132
- // ------------------------------------------------
103
+ }
133
104
 
134
- // הגדרת אירועים (Events)
135
- $('#btn-chat-search').off('click').on('click', executeSearch);
136
-
137
- // שמירת גלילה
138
- results.on('scroll', function() {
139
- window.chatSearchState.lastScroll = $(this).scrollTop();
140
- });
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}`;
141
110
 
142
- input.off('keypress').on('keypress', function (e) {
143
- if (e.which === 13) executeSearch();
144
- });
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
+ }
145
114
 
146
- input.on('input', function() {
147
- window.chatSearchState.query = $(this).val();
148
- });
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();
149
134
  }
150
135
 
151
136
  function executeSearch() {
@@ -160,20 +145,14 @@ $(document).ready(function () {
160
145
  }
161
146
 
162
147
  let targetUid = ajaxify.data.uid || app.user.uid;
163
-
164
148
  resultsContainer.show().html('<div class="text-center" style="padding:10px;"><i class="fa fa-spinner fa-spin"></i> מחפש...</div>');
165
149
  window.chatSearchState.isOpen = true;
166
150
 
167
- socket.emit('plugins.chatSearch.searchGlobal', {
168
- query: query,
169
- targetUid: targetUid
170
- }, function (err, messages) {
151
+ socket.emit('plugins.chatSearch.searchGlobal', { query: query, targetUid: targetUid }, function (err, messages) {
171
152
  if (err) {
172
- console.error(err);
173
153
  resultsContainer.html('<div class="alert alert-danger" style="margin:5px;">שגיאה</div>');
174
154
  return;
175
155
  }
176
-
177
156
  if (!messages || messages.length === 0) {
178
157
  const noRes = '<div class="text-center" style="padding:10px; color:#777;">לא נמצאו תוצאות.</div>';
179
158
  resultsContainer.html(noRes);
@@ -181,59 +160,82 @@ $(document).ready(function () {
181
160
  return;
182
161
  }
183
162
 
184
- let html = '<ul class="list-group" style="margin:0;">';
185
-
163
+ let html = '<div class="d-flex flex-column">';
186
164
  messages.forEach(msg => {
187
- const date = new Date(msg.timestamp).toLocaleDateString();
165
+ const isoTime = new Date(msg.timestamp).toISOString();
166
+
188
167
  let baseUrl = window.location.pathname.replace(/\/chats\/.*$/, '/chats');
189
168
  if (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1);
190
-
169
+
191
170
  const chatLink = baseUrl + '/' + msg.roomId + '?mid=' + msg.mid;
192
171
  const senderName = (msg.user && msg.user.username) ? msg.user.username : 'Unknown';
172
+
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);
193
177
 
194
178
  html += `
195
- <li class="list-group-item search-result" data-roomid="${msg.roomId}" style="cursor:pointer; border-bottom: 1px solid #f0f0f0; padding: 10px 8px;" onclick="ajaxify.go('${chatLink}')">
196
- <div style="font-size:13px; color:#333; margin-bottom:5px;">
197
- <span class="pull-left text-muted" style="font-size:10px; margin-right: 8px;">${date}</span>
198
- <strong>${msg.roomName}</strong>
199
- </div>
200
- <div style="font-size:12px; color:#444; background: #f9f9f9; padding: 6px; border-radius: 4px; border-right: 3px solid #007bff;">
201
- <strong>${senderName}:</strong> ${msg.content}
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>
202
204
  </div>
203
- </li>
205
+ </div>
206
+ <hr class="my-1">
204
207
  `;
205
208
  });
206
- html += '</ul>';
209
+ html += '</div>';
207
210
 
208
211
  resultsContainer.html(html);
209
212
 
210
- // עדכון הזיכרון
213
+ if ($.fn.timeago) {
214
+ resultsContainer.find('.timeago').timeago();
215
+ }
216
+
211
217
  window.chatSearchState.resultsHtml = html;
212
218
  window.chatSearchState.lastScroll = 0;
213
-
214
219
  highlightActiveChat();
215
220
  });
216
221
  }
217
222
 
218
223
  function highlightActiveChat() {
219
- // מנסים לקחת RoomID מה-URL או מהמידע של NodeBB
220
224
  let currentRoomId = ajaxify.data.roomId;
221
225
  if (!currentRoomId) {
222
226
  const match = window.location.pathname.match(/chats\/(\d+)/);
223
227
  if (match) currentRoomId = match[1];
224
228
  }
225
-
226
229
  if (!currentRoomId) return;
227
-
228
- $('.search-result').css('background-color', '');
229
- $('.search-result[data-roomid="' + currentRoomId + '"]').css('background-color', '#eef6ff');
230
+ $('.search-result').removeClass('active');
231
+ const activeItem = $('.search-result[data-roomid="' + currentRoomId + '"]');
232
+ activeItem.addClass('active');
230
233
  }
231
234
 
232
235
  function handleScrollToMessage() {
233
236
  const params = new URLSearchParams(window.location.search);
234
237
  const mid = params.get('mid');
235
238
  if (!mid) return;
236
-
237
239
  scrollToId(mid);
238
240
  let attempts = 0;
239
241
  const scrollInt = setInterval(() => {