diff --git a/shine-UI/js/pages/network-view.js b/shine-UI/js/pages/network-view.js
index 6d88038..fd30c3d 100644
--- a/shine-UI/js/pages/network-view.js
+++ b/shine-UI/js/pages/network-view.js
@@ -10,6 +10,65 @@ function makeNode(name, cls = '') {
return n;
}
+function showAddCloseFriendModal({ onAdded }) {
+ const root = document.getElementById('modal-root');
+ root.innerHTML = `
+
+
+
Добавить близкого друга
+
+
+
+
+
+
+
+
+ `;
+
+ const close = () => { root.innerHTML = ''; };
+ root.querySelector('#close-friend-back').addEventListener('click', close);
+
+ root.querySelector('#close-friend-search').addEventListener('click', async () => {
+ const query = root.querySelector('#close-friend-query').value.trim();
+ const holder = root.querySelector('#close-friend-results');
+ holder.innerHTML = 'Поиск...
';
+
+ try {
+ const logins = await authService.searchUsers(query);
+ holder.innerHTML = '';
+ if (!logins.length) {
+ holder.innerHTML = 'Пользователи не найдены.
';
+ return;
+ }
+
+ logins.forEach((login) => {
+ const row = document.createElement('article');
+ row.className = 'list-item';
+ row.innerHTML = `
+ ${(login[0] || '?').toUpperCase()}
+
+ Добавить
+ `;
+ row.addEventListener('click', async () => {
+ const yes = window.confirm(`Добавить ${login} в близкие друзья?`);
+ if (!yes) return;
+ try {
+ await authService.addCloseFriend(login);
+ close();
+ if (typeof onAdded === 'function') await onAdded();
+ } catch (e) {
+ window.alert(`Ошибка добавления: ${e.message || 'unknown'}`);
+ }
+ });
+ holder.append(row);
+ });
+ } catch (e) {
+ holder.innerHTML = `Ошибка поиска: ${e.message || 'unknown'}
`;
+ }
+ });
+}
+
export function render() {
const screen = document.createElement('section');
screen.className = 'stack';
@@ -62,14 +121,20 @@ export function render() {
right.forEach((name, i) => svg.append(mk(name, 'right', i, right.length)));
board.prepend(svg);
- note.textContent = 'Нажмите на любой узел, чтобы построить связи выбранного пользователя.';
+ note.textContent = 'Нажмите на узел, чтобы перестроить связи вокруг выбранного пользователя.';
} catch (e) {
note.textContent = `Ошибка загрузки связей: ${e.message || 'unknown'}`;
}
};
+ const addBtn = document.createElement('button');
+ addBtn.className = 'primary-btn';
+ addBtn.type = 'button';
+ addBtn.textContent = 'Добавить близкого друга';
+ addBtn.addEventListener('click', () => showAddCloseFriendModal({ onAdded: () => load() }));
+
load();
- screen.append(renderHeader({ title: 'Связи' }), board, note);
+ screen.append(renderHeader({ title: 'Связи' }), addBtn, board, note);
return screen;
}
diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js
index 00f79f7..bd2afb5 100644
--- a/shine-UI/js/services/auth-service.js
+++ b/shine-UI/js/services/auth-service.js
@@ -264,6 +264,13 @@ export class AuthService {
return response.payload || {};
}
+
+ async addCloseFriend(toLogin) {
+ const response = await this.ws.request('AddCloseFriend', { toLogin });
+ if (response.status !== 200) throw opError('AddCloseFriend', response);
+ return response.payload || {};
+ }
+
async getUserConnectionsGraph(login) {
const response = await this.ws.request('GetUserConnectionsGraph', { login });
if (response.status !== 200) throw opError('GetUserConnectionsGraph', response);
diff --git a/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java b/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java
index 3d03346..1e910bf 100644
--- a/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java
+++ b/shine-server-db/src/main/java/shine/db/dao/ConnectionsStateDAO.java
@@ -125,4 +125,32 @@ public final class ConnectionsStateDAO {
}
return out;
}
-}
\ No newline at end of file
+
+ public void upsertRelation(Connection c,
+ String login,
+ int relType,
+ String toLogin,
+ String toBchName,
+ Integer toBlockNumber,
+ byte[] toBlockHash) throws SQLException {
+ String sql = """
+ INSERT INTO connections_state (login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash)
+ VALUES (?, ?, ?, ?, ?, ?)
+ ON CONFLICT(login, rel_type, to_login) DO UPDATE SET
+ to_bch_name=excluded.to_bch_name,
+ to_block_number=excluded.to_block_number,
+ to_block_hash=excluded.to_block_hash
+ """;
+
+ try (PreparedStatement ps = c.prepareStatement(sql)) {
+ ps.setString(1, login);
+ ps.setInt(2, relType);
+ ps.setString(3, toLogin);
+ ps.setString(4, toBchName);
+ if (toBlockNumber == null) ps.setNull(5, java.sql.Types.INTEGER); else ps.setInt(5, toBlockNumber);
+ ps.setBytes(6, toBlockHash);
+ ps.executeUpdate();
+ }
+ }
+
+}
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java
index 604f7b7..5862635 100644
--- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java
@@ -52,8 +52,10 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMe
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
+import server.logic.ws_protocol.JSON.handlers.connections.Net_AddCloseFriend_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
+import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
import server.logic.ws_protocol.JSON.messages.Net_AckIncomingMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
@@ -108,6 +110,7 @@ public final class JsonHandlerRegistry {
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
Map.entry("ListContacts", new Net_ListContacts_Handler()),
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
+ Map.entry("AddCloseFriend", new Net_AddCloseFriend_Handler()),
// --- direct messages / push ---
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
@@ -153,6 +156,7 @@ public final class JsonHandlerRegistry {
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
Map.entry("ListContacts", Net_ListContacts_Request.class),
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
+ Map.entry("AddCloseFriend", Net_AddCloseFriend_Request.class),
// --- direct messages / push ---
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java
new file mode 100644
index 0000000..3cfe34f
--- /dev/null
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_AddCloseFriend_Handler.java
@@ -0,0 +1,85 @@
+package server.logic.ws_protocol.JSON.handlers.connections;
+
+import server.logic.ws_protocol.JSON.ConnectionContext;
+import server.logic.ws_protocol.JSON.entyties.Net_Request;
+import server.logic.ws_protocol.JSON.entyties.Net_Response;
+import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
+import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Request;
+import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseFriend_Response;
+import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
+import server.logic.ws_protocol.WireCodes;
+import shine.db.MsgSubType;
+import shine.db.dao.ConnectionsStateDAO;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+
+public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
+ @Override
+ public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
+ Net_AddCloseFriend_Request req = (Net_AddCloseFriend_Request) baseRequest;
+ if (ctx == null || !ctx.isAuthenticatedUser()) {
+ return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
+ }
+ if (req.getToLogin() == null || req.getToLogin().isBlank()) {
+ return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin обязателен");
+ }
+
+ String from = ctx.getLogin();
+ String toLogin = req.getToLogin().trim();
+ if (from.equalsIgnoreCase(toLogin)) {
+ return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Нельзя добавить себя");
+ }
+
+ try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
+ String canonicalTo = findCanonicalLogin(c, toLogin);
+ if (canonicalTo == null) {
+ return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
+ }
+ String targetBch = findPrimaryBlockchain(c, canonicalTo);
+ if (targetBch == null) {
+ return NetExceptionResponseFactory.error(req, 404, "BLOCKCHAIN_NOT_FOUND", "У пользователя нет blockchain");
+ }
+
+ ConnectionsStateDAO.getInstance().upsertRelation(
+ c,
+ from,
+ MsgSubType.CONNECTION_FRIEND,
+ canonicalTo,
+ targetBch,
+ 0,
+ new byte[32]
+ );
+
+ Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response();
+ resp.setOp(req.getOp());
+ resp.setRequestId(req.getRequestId());
+ resp.setStatus(WireCodes.Status.OK);
+ resp.setLogin(from);
+ resp.setToLogin(canonicalTo);
+ resp.setRelation("close_friend");
+ return resp;
+ }
+ }
+
+ private String findCanonicalLogin(Connection c, String login) throws Exception {
+ String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1";
+ try (PreparedStatement ps = c.prepareStatement(sql)) {
+ ps.setString(1, login);
+ try (ResultSet rs = ps.executeQuery()) {
+ return rs.next() ? rs.getString("login") : null;
+ }
+ }
+ }
+
+ private String findPrimaryBlockchain(Connection c, String login) throws Exception {
+ String sql = "SELECT blockchain_name FROM blockchain_state WHERE login=? ORDER BY blockchain_name LIMIT 1";
+ try (PreparedStatement ps = c.prepareStatement(sql)) {
+ ps.setString(1, login);
+ try (ResultSet rs = ps.executeQuery()) {
+ return rs.next() ? rs.getString("blockchain_name") : null;
+ }
+ }
+ }
+}
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java
new file mode 100644
index 0000000..76caef3
--- /dev/null
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Request.java
@@ -0,0 +1,10 @@
+package server.logic.ws_protocol.JSON.handlers.connections.entyties;
+
+import server.logic.ws_protocol.JSON.entyties.Net_Request;
+
+public class Net_AddCloseFriend_Request extends Net_Request {
+ private String toLogin;
+
+ public String getToLogin() { return toLogin; }
+ public void setToLogin(String toLogin) { this.toLogin = toLogin; }
+}
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java
new file mode 100644
index 0000000..5e5b07e
--- /dev/null
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_AddCloseFriend_Response.java
@@ -0,0 +1,16 @@
+package server.logic.ws_protocol.JSON.handlers.connections.entyties;
+
+import server.logic.ws_protocol.JSON.entyties.Net_Response;
+
+public class Net_AddCloseFriend_Response extends Net_Response {
+ private String login;
+ private String toLogin;
+ private String relation;
+
+ public String getLogin() { return login; }
+ public void setLogin(String login) { this.login = login; }
+ public String getToLogin() { return toLogin; }
+ public void setToLogin(String toLogin) { this.toLogin = toLogin; }
+ public String getRelation() { return relation; }
+ public void setRelation(String relation) { this.relation = relation; }
+}