nodebb-plugin-chat-search 0.0.1
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 +101 -0
- package/package.json +10 -0
- package/plugin.json +13 -0
- package/static/lib/main.js +157 -0
package/library.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const messaging = require.main.require('./src/messaging');
|
|
4
|
+
const user = require.main.require('./src/user');
|
|
5
|
+
const db = require.main.require('./src/database');
|
|
6
|
+
|
|
7
|
+
const plugin = {};
|
|
8
|
+
|
|
9
|
+
plugin.init = async (params) => {
|
|
10
|
+
const socketPlugins = require.main.require('./src/socket.io/plugins');
|
|
11
|
+
socketPlugins.chatSearch = {};
|
|
12
|
+
socketPlugins.chatSearch.searchGlobal = searchGlobal;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
plugin.addClientScript = async (scripts) => {
|
|
16
|
+
scripts.push('plugins/nodebb-plugin-chat-search/static/lib/main.js');
|
|
17
|
+
return scripts;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function searchGlobal(socket, data) {
|
|
21
|
+
if (!socket.uid) {
|
|
22
|
+
throw new Error('Not logged in');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let targetUid = socket.uid;
|
|
26
|
+
// בדיקת הרשאות (אדמין צופה במישהו אחר)
|
|
27
|
+
if (data.targetUid && parseInt(data.targetUid, 10) !== parseInt(socket.uid, 10)) {
|
|
28
|
+
const isAdmin = await user.isAdministrator(socket.uid);
|
|
29
|
+
if (!isAdmin) {
|
|
30
|
+
throw new Error('אין הרשאה.');
|
|
31
|
+
}
|
|
32
|
+
targetUid = data.targetUid;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const query = data.query;
|
|
36
|
+
const roomIds = await db.getSortedSetRevRange('uid:' + targetUid + ':chat:rooms', 0, -1);
|
|
37
|
+
|
|
38
|
+
let allResults = [];
|
|
39
|
+
|
|
40
|
+
for (const roomId of roomIds) {
|
|
41
|
+
// בדיקת חברות
|
|
42
|
+
const inRoom = await messaging.isUserInRoom(targetUid, roomId);
|
|
43
|
+
if (!inRoom) continue;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 1. שליפת הודעות
|
|
47
|
+
const messages = await messaging.getMessages({
|
|
48
|
+
callerUid: socket.uid,
|
|
49
|
+
uid: targetUid,
|
|
50
|
+
roomId: roomId,
|
|
51
|
+
isNew: false,
|
|
52
|
+
start: 0,
|
|
53
|
+
stop: -1
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!messages || !Array.isArray(messages)) continue;
|
|
57
|
+
|
|
58
|
+
const matches = messages.filter(msg =>
|
|
59
|
+
msg.content && msg.content.toLowerCase().includes(query.toLowerCase())
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (matches.length > 0) {
|
|
63
|
+
// --- תיקון שליפת המשתתפים ---
|
|
64
|
+
// במקום להסתמך על roomData, נשלוף UIDs ואז Users
|
|
65
|
+
const uids = await messaging.getUidsInRoom(roomId, 0, -1);
|
|
66
|
+
const usersData = await user.getUsersFields(uids, ['uid', 'username', 'picture']);
|
|
67
|
+
|
|
68
|
+
// סינון המשתמש שבו אנו צופים
|
|
69
|
+
const participants = usersData
|
|
70
|
+
.filter(u => parseInt(u.uid, 10) !== parseInt(targetUid, 10))
|
|
71
|
+
.map(u => u.username)
|
|
72
|
+
.join(', ');
|
|
73
|
+
|
|
74
|
+
// --- תיקון שליפת שם החדר ---
|
|
75
|
+
const roomData = await messaging.getRoomData(roomId);
|
|
76
|
+
let roomName = (roomData && roomData.roomName) || participants || `חדר ${roomId}`;
|
|
77
|
+
|
|
78
|
+
// --- תיקון שם השולח (במקרה שחסר בהודעה) ---
|
|
79
|
+
// אם חסר אובייקט user בהודעה, נשלים אותו מתוך usersData ששלפנו הרגע
|
|
80
|
+
matches.forEach(m => {
|
|
81
|
+
if (!m.user || !m.user.username) {
|
|
82
|
+
const sender = usersData.find(u => parseInt(u.uid, 10) === parseInt(m.fromuid, 10));
|
|
83
|
+
m.user = sender || { username: 'Unknown' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
m.roomName = roomName;
|
|
87
|
+
m.chatWith = participants;
|
|
88
|
+
m.targetUid = targetUid;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
allResults = allResults.concat(matches);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[Chat Search] Error in room ${roomId}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return allResults;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = plugin;
|
package/package.json
ADDED
package/plugin.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-chat-search",
|
|
3
|
+
"name": "Chat Search",
|
|
4
|
+
"description": "Allows searching in chat windows",
|
|
5
|
+
"library": "./library.js",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
8
|
+
{ "hook": "filter:scripts.client", "method": "addClientScript" }
|
|
9
|
+
],
|
|
10
|
+
"scripts": [
|
|
11
|
+
"static/lib/main.js"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
$(document).ready(function () {
|
|
4
|
+
console.log('[Chat Search] Loaded (AJAX Fix).');
|
|
5
|
+
|
|
6
|
+
// בכל מעבר דף, אנחנו מוחקים את הישן ומתחילים חיפוש חדש של הקונטיינר
|
|
7
|
+
$(window).on('action:ajaxify.end', function (ev, data) {
|
|
8
|
+
$('#global-chat-search-container').remove(); // איפוס
|
|
9
|
+
|
|
10
|
+
// בדיקה: האם אנחנו בעמוד צ'אטים?
|
|
11
|
+
// גם לפי URL וגם לפי Template
|
|
12
|
+
const isChatUrl = data.url.match(/^(user\/[^\/]+\/)?chats/);
|
|
13
|
+
const isChatTemplate = data.template && data.template.name === 'chats';
|
|
14
|
+
|
|
15
|
+
if (isChatUrl || isChatTemplate) {
|
|
16
|
+
waitForElementAndAdd();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
$(window).on('action:chat.loaded', function (ev, data) {
|
|
21
|
+
handleScrollToMessage();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// הרצה ראשונית (למקרה של ריענון מלא)
|
|
25
|
+
if (ajaxify.data.template && ajaxify.data.template.name === 'chats') {
|
|
26
|
+
waitForElementAndAdd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function waitForElementAndAdd() {
|
|
30
|
+
let attempts = 0;
|
|
31
|
+
const interval = setInterval(function() {
|
|
32
|
+
attempts++;
|
|
33
|
+
|
|
34
|
+
// חיפוש הקונטיינר החדש שנוצר
|
|
35
|
+
let container = $('[component="chat/nav-wrapper"]');
|
|
36
|
+
if (container.length === 0) container = $('.chats-page').find('.col-md-4').first();
|
|
37
|
+
|
|
38
|
+
// ברגע שמצאנו - מזריקים ועוצרים
|
|
39
|
+
if (container.length > 0) {
|
|
40
|
+
addGlobalSearchBar(container);
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
} else if (attempts >= 20) { // מנסים במשך 4 שניות
|
|
43
|
+
clearInterval(interval);
|
|
44
|
+
}
|
|
45
|
+
}, 200); // בדיקה מהירה כל 200ms
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function addGlobalSearchBar(container) {
|
|
49
|
+
if ($('#global-chat-search-container').length > 0) return;
|
|
50
|
+
|
|
51
|
+
const searchHtml = `
|
|
52
|
+
<div id="global-chat-search-container" style="padding: 10px; background: #fff; border-bottom: 1px solid #ddd; margin-bottom: 10px;">
|
|
53
|
+
<div class="input-group">
|
|
54
|
+
<input type="text" id="global-chat-search" class="form-control" placeholder="חפש הודעה..." style="font-size: 14px;">
|
|
55
|
+
<span class="input-group-btn">
|
|
56
|
+
<button class="btn btn-primary" id="btn-chat-search" type="button"><i class="fa fa-search"></i></button>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="global-search-results" style="margin-top: 5px; max-height: 400px; overflow-y: auto; background: white; border: 1px solid #eee; display:none;"></div>
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
container.prepend(searchHtml);
|
|
64
|
+
|
|
65
|
+
$('#btn-chat-search').off('click').on('click', executeSearch);
|
|
66
|
+
$('#global-chat-search').off('keypress').on('keypress', function (e) {
|
|
67
|
+
if (e.which === 13) executeSearch();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function executeSearch() {
|
|
72
|
+
const query = $('#global-chat-search').val();
|
|
73
|
+
const resultsContainer = $('#global-search-results');
|
|
74
|
+
|
|
75
|
+
if (!query) {
|
|
76
|
+
resultsContainer.hide();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let targetUid = ajaxify.data.uid || app.user.uid;
|
|
81
|
+
|
|
82
|
+
resultsContainer.show().html('<div class="text-center" style="padding:10px;"><i class="fa fa-spinner fa-spin"></i> מחפש...</div>');
|
|
83
|
+
|
|
84
|
+
socket.emit('plugins.chatSearch.searchGlobal', {
|
|
85
|
+
query: query,
|
|
86
|
+
targetUid: targetUid
|
|
87
|
+
}, function (err, messages) {
|
|
88
|
+
if (err) {
|
|
89
|
+
console.error(err);
|
|
90
|
+
resultsContainer.html('<div class="alert alert-danger" style="margin:5px;">שגיאה בחיפוש</div>');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!messages || messages.length === 0) {
|
|
95
|
+
resultsContainer.html('<div class="text-center" style="padding:10px; color:#777;">לא נמצאו תוצאות.</div>');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let html = '<ul class="list-group" style="margin:0;">';
|
|
100
|
+
|
|
101
|
+
messages.forEach(msg => {
|
|
102
|
+
const date = new Date(msg.timestamp).toLocaleDateString();
|
|
103
|
+
|
|
104
|
+
let baseUrl = window.location.pathname;
|
|
105
|
+
if (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1);
|
|
106
|
+
if (baseUrl.match(/\/[0-9]+$/)) baseUrl = baseUrl.replace(/\/[0-9]+$/, '');
|
|
107
|
+
|
|
108
|
+
const chatLink = baseUrl + '/' + msg.roomId + '?mid=' + msg.mid;
|
|
109
|
+
const senderName = (msg.user && msg.user.username) ? msg.user.username : 'Unknown';
|
|
110
|
+
|
|
111
|
+
let participantsHtml = '';
|
|
112
|
+
if (msg.chatWith) {
|
|
113
|
+
participantsHtml = `<div style="font-size:11px; color:#005999; margin-bottom:2px;"><i class="fa fa-users"></i> עם: <strong>${msg.chatWith}</strong></div>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
html += `
|
|
117
|
+
<li class="list-group-item search-result" style="cursor:pointer; border-bottom: 1px solid #f0f0f0; padding: 8px;" onclick="ajaxify.go('${chatLink}')">
|
|
118
|
+
<div style="font-size:13px; color:#333; margin-bottom:2px;">
|
|
119
|
+
<strong>${msg.roomName}</strong>
|
|
120
|
+
<span class="pull-left text-muted" style="font-size:10px;">${date}</span>
|
|
121
|
+
</div>
|
|
122
|
+
${participantsHtml}
|
|
123
|
+
<div style="font-size:12px; color:#444; background: #f9f9f9; padding: 4px; border-radius: 3px; border-right: 2px solid #007bff;">
|
|
124
|
+
<strong>${senderName}:</strong> ${msg.content}
|
|
125
|
+
</div>
|
|
126
|
+
</li>
|
|
127
|
+
`;
|
|
128
|
+
});
|
|
129
|
+
html += '</ul>';
|
|
130
|
+
resultsContainer.html(html);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleScrollToMessage() {
|
|
135
|
+
const params = new URLSearchParams(window.location.search);
|
|
136
|
+
const mid = params.get('mid');
|
|
137
|
+
if (!mid) return;
|
|
138
|
+
|
|
139
|
+
scrollToId(mid);
|
|
140
|
+
let attempts = 0;
|
|
141
|
+
const scrollInt = setInterval(() => {
|
|
142
|
+
attempts++;
|
|
143
|
+
if (scrollToId(mid) || attempts > 10) clearInterval(scrollInt);
|
|
144
|
+
}, 500);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function scrollToId(mid) {
|
|
148
|
+
const el = $('[data-mid="' + mid + '"]');
|
|
149
|
+
if (el.length > 0) {
|
|
150
|
+
el[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
151
|
+
el.css('background', '#fffeca').css('transition', 'background 1s');
|
|
152
|
+
setTimeout(() => el.css('background', ''), 2000);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
});
|