feat(relations): spouse 40/41 и новый UX вкладки Связи (проверено)

This commit is contained in:
AidarKC 2026-04-26 18:24:30 +03:00
parent 3e10407afd
commit 1fec6c7b54
12 changed files with 292 additions and 131 deletions

View File

@ -1,2 +1,2 @@
client.version=1.2.11 client.version=1.2.12
server.version=1.2.11 server.version=1.2.12

View File

@ -1,6 +1,7 @@
import { renderHeader } from '../components/header.js'; import { renderHeader } from '../components/header.js';
import { authService, state } from '../state.js'; import { authService, state } from '../state.js';
import { renderUserAvatar } from '../components/avatar-image.js'; import { renderUserAvatar } from '../components/avatar-image.js';
import { loadUserProfileCard } from '../services/user-connections.js';
export const pageMeta = { id: 'network-view', title: 'Связи' }; export const pageMeta = { id: 'network-view', title: 'Связи' };
@ -87,9 +88,94 @@ function getRelativeGenderMap(graph) {
applyRelativeGender(map, graph?.parents); applyRelativeGender(map, graph?.parents);
applyRelativeGender(map, graph?.children); applyRelativeGender(map, graph?.children);
applyRelativeGender(map, graph?.siblings); applyRelativeGender(map, graph?.siblings);
applyRelativeGender(map, graph?.spouses);
return map; return map;
} }
function relativeRoleLabel(role, gender) {
const cleanGender = normalizeGender(gender);
if (role === 'parent') {
if (cleanGender === GENDER_MALE) return 'отец';
if (cleanGender === GENDER_FEMALE) return 'мать';
return 'родитель';
}
if (role === 'child') {
if (cleanGender === GENDER_MALE) return 'сын';
if (cleanGender === GENDER_FEMALE) return 'дочь';
return 'потомок';
}
if (role === 'sibling') {
if (cleanGender === GENDER_MALE) return 'брат';
if (cleanGender === GENDER_FEMALE) return 'сестра';
return 'брат/сестра';
}
if (role === 'spouse') {
if (cleanGender === GENDER_MALE) return 'муж';
if (cleanGender === GENDER_FEMALE) return 'жена';
return 'жена/муж';
}
return '';
}
function buildNameLines(firstName, lastName) {
const first = String(firstName || '').trim();
const last = String(lastName || '').trim();
if (first && last) {
const full = `${first} ${last}`.trim();
if (full.length <= 20) return [full];
return [first, last];
}
if (first) return [first];
if (last) return [last];
return [];
}
function applyNodeText(node, {
login,
firstName = '',
lastName = '',
role = 'friend',
gender = GENDER_UNKNOWN,
mark = null,
} = {}) {
const loginText = normalizeLogin(login);
const labelsWrap = node.querySelector('.node-label');
const nameEl = node.querySelector('.node-name');
const loginEl = node.querySelector('.node-login');
const relationEl = node.querySelector('.node-relation');
if (!(labelsWrap instanceof HTMLElement) || !(nameEl instanceof HTMLElement) || !(loginEl instanceof HTMLElement)) {
return;
}
const nameLines = buildNameLines(firstName, lastName);
nameEl.innerHTML = '';
if (nameLines.length) {
nameLines.forEach((line) => {
const lineEl = document.createElement('span');
lineEl.className = 'node-name-line';
lineEl.textContent = line;
nameEl.append(lineEl);
});
labelsWrap.classList.remove('is-login-only');
} else {
labelsWrap.classList.add('is-login-only');
}
loginEl.textContent = loginText;
const relLabel = relativeRoleLabel(role, gender);
if (relationEl instanceof HTMLElement) {
relationEl.textContent = relLabel;
relationEl.hidden = !relLabel;
}
const metaParts = [];
if (mark?.officialLabel) metaParts.push(mark.officialLabel);
if (mark?.shineLabel) metaParts.push(mark.shineLabel);
if (relLabel) metaParts.push(`роль: ${relLabel}`);
const titleMain = nameLines.length ? `${nameLines.join(' ')} (${loginText})` : loginText;
node.title = metaParts.length ? `${titleMain}\n${metaParts.join(', ')}` : titleMain;
}
function spread(count, start, end) { function spread(count, start, end) {
if (count <= 0) return []; if (count <= 0) return [];
if (count === 1) return [(start + end) / 2]; if (count === 1) return [(start + end) / 2];
@ -99,7 +185,16 @@ function spread(count, start, end) {
return out; return out;
} }
function buildNodeElement({ login, kind = 'friend', isCenter = false, mark = null }) { function buildNodeElement({
login,
kind = 'friend',
isCenter = false,
mark = null,
role = 'friend',
gender = GENDER_UNKNOWN,
firstName = '',
lastName = '',
}) {
const node = document.createElement('button'); const node = document.createElement('button');
node.type = 'button'; node.type = 'button';
const classes = [ const classes = [
@ -112,12 +207,6 @@ function buildNodeElement({ login, kind = 'friend', isCenter = false, mark = nul
node.className = classes.join(' '); node.className = classes.join(' ');
node.dataset.nodeLogin = login; node.dataset.nodeLogin = login;
const metaParts = [];
if (mark?.officialLabel) metaParts.push(mark.officialLabel);
if (mark?.shineLabel) metaParts.push(mark.shineLabel);
const metaText = metaParts.join(', ');
node.title = metaText ? `${login}\n${metaText}` : login;
if (mark?.official) { if (mark?.official) {
const officialBadge = document.createElement('span'); const officialBadge = document.createElement('span');
officialBadge.className = 'node-badge-official'; officialBadge.className = 'node-badge-official';
@ -131,10 +220,16 @@ function buildNodeElement({ login, kind = 'friend', isCenter = false, mark = nul
size: 'node', size: 'node',
title: login, title: login,
})); }));
const label = document.createElement('span'); const label = document.createElement('span');
label.className = 'node-label'; label.className = 'node-label';
label.textContent = login; label.innerHTML = `
<span class="node-name"></span>
<span class="node-login"></span>
<span class="node-relation" hidden></span>
`;
node.append(label); node.append(label);
applyNodeText(node, { login, firstName, lastName, role, gender, mark });
return node; return node;
} }
@ -148,6 +243,8 @@ function buildGraphModel(graph, centerLogin) {
const inChildren = toSet(graph?.inChildren); const inChildren = toSet(graph?.inChildren);
const outSiblings = toSet(graph?.outSiblings); const outSiblings = toSet(graph?.outSiblings);
const inSiblings = toSet(graph?.inSiblings); const inSiblings = toSet(graph?.inSiblings);
const outSpouses = toSet(graph?.outSpouses);
const inSpouses = toSet(graph?.inSpouses);
const relativesGender = getRelativeGenderMap(graph); const relativesGender = getRelativeGenderMap(graph);
const allMarks = getMarkByLogin(graph?.allUsers); const allMarks = getMarkByLogin(graph?.allUsers);
@ -161,6 +258,8 @@ function buildGraphModel(graph, centerLogin) {
...(graph?.inChildren || []), ...(graph?.inChildren || []),
...(graph?.outSiblings || []), ...(graph?.outSiblings || []),
...(graph?.inSiblings || []), ...(graph?.inSiblings || []),
...(graph?.outSpouses || []),
...(graph?.inSpouses || []),
]).filter((entry) => normKey(entry) !== normKey(login)); ]).filter((entry) => normKey(entry) !== normKey(login));
const relations = allLogins.map((targetLogin) => { const relations = allLogins.map((targetLogin) => {
@ -170,12 +269,15 @@ function buildGraphModel(graph, centerLogin) {
const childIn = hasLogin(inParents, targetLogin); const childIn = hasLogin(inParents, targetLogin);
const siblingOut = hasLogin(outSiblings, targetLogin); const siblingOut = hasLogin(outSiblings, targetLogin);
const siblingIn = hasLogin(inSiblings, targetLogin); const siblingIn = hasLogin(inSiblings, targetLogin);
const spouseOut = hasLogin(outSpouses, targetLogin);
const spouseIn = hasLogin(inSpouses, targetLogin);
const friendOut = hasLogin(outFriends, targetLogin); const friendOut = hasLogin(outFriends, targetLogin);
const friendIn = hasLogin(inFriends, targetLogin); const friendIn = hasLogin(inFriends, targetLogin);
let role = 'friend'; let role = 'friend';
if (parentOut || parentIn) role = 'parent'; if (parentOut || parentIn) role = 'parent';
else if (childOut || childIn) role = 'child'; else if (childOut || childIn) role = 'child';
else if (spouseOut || spouseIn) role = 'spouse';
else if (siblingOut || siblingIn) role = 'sibling'; else if (siblingOut || siblingIn) role = 'sibling';
let forward = friendOut; let forward = friendOut;
@ -186,6 +288,9 @@ function buildGraphModel(graph, centerLogin) {
} else if (role === 'child') { } else if (role === 'child') {
forward = childOut; forward = childOut;
backward = childIn; backward = childIn;
} else if (role === 'spouse') {
forward = spouseOut;
backward = spouseIn;
} else if (role === 'sibling') { } else if (role === 'sibling') {
forward = siblingOut; forward = siblingOut;
backward = siblingIn; backward = siblingIn;
@ -240,16 +345,19 @@ function layoutNodes(model) {
isCenter: true, isCenter: true,
kind: 'center', kind: 'center',
relation: null, relation: null,
gender: GENDER_UNKNOWN,
mark: model.centerMark, mark: model.centerMark,
}; };
const parents = sortByLogin(model.relations.filter((item) => item.role === 'parent')); const parents = sortByLogin(model.relations.filter((item) => item.role === 'parent'));
const children = sortByLogin(model.relations.filter((item) => item.role === 'child')); const children = sortByLogin(model.relations.filter((item) => item.role === 'child'));
const spouses = sortByLogin(model.relations.filter((item) => item.role === 'spouse'));
const siblings = sortByLogin(model.relations.filter((item) => item.role === 'sibling')); const siblings = sortByLogin(model.relations.filter((item) => item.role === 'sibling'));
const friends = sortByLogin(model.relations.filter((item) => item.role === 'friend')); const friends = sortByLogin(model.relations.filter((item) => item.role === 'friend'));
const parentSplit = splitByGender(parents); const parentSplit = splitByGender(parents);
const childSplit = splitByGender(children); const childSplit = splitByGender(children);
const spouseSplit = splitByGender(spouses);
const siblingSplit = splitByGender(siblings); const siblingSplit = splitByGender(siblings);
const friendLeft = []; const friendLeft = [];
@ -260,17 +368,20 @@ function layoutNodes(model) {
}); });
const positioned = [ const positioned = [
...positionRows(parentSplit.left, 28, 14, 30), ...positionRows(parentSplit.left, 28, 16, 28),
...positionRows(parentSplit.right, 72, 14, 30), ...positionRows(parentSplit.right, 72, 16, 28),
...positionRows(parentSplit.center, 50, 10, 22), ...positionRows(parentSplit.center, 50, 10, 22),
...positionRows(friendLeft, 12, 28, 72), ...positionRows(friendLeft, 12, 30, 70),
...positionRows(friendRight, 88, 28, 72), ...positionRows(friendRight, 88, 30, 70),
...positionRows(siblingSplit.left, 30, 48, 70), ...positionRows(spouseSplit.left, 36, 38, 58),
...positionRows(siblingSplit.right, 70, 48, 70), ...positionRows(spouseSplit.right, 64, 38, 58),
...positionRows(siblingSplit.center, 50, 58, 74), ...positionRows(spouseSplit.center, 50, 40, 56),
...positionRows(childSplit.left, 28, 70, 90), ...positionRows(siblingSplit.left, 30, 46, 66),
...positionRows(childSplit.right, 72, 70, 90), ...positionRows(siblingSplit.right, 70, 46, 66),
...positionRows(childSplit.center, 50, 82, 94), ...positionRows(siblingSplit.center, 50, 54, 70),
...positionRows(childSplit.left, 28, 68, 84),
...positionRows(childSplit.right, 72, 68, 84),
...positionRows(childSplit.center, 50, 78, 88),
]; ];
const nodes = [centerNode]; const nodes = [centerNode];
@ -286,6 +397,7 @@ function layoutNodes(model) {
isCenter: false, isCenter: false,
kind: item.isRelative ? 'relative' : 'friend', kind: item.isRelative ? 'relative' : 'friend',
relation: item.role, relation: item.role,
gender: item.gender,
mark: item.mark, mark: item.mark,
}); });
edges.push({ edges.push({
@ -401,90 +513,74 @@ export function render({ navigate }) {
<span><i class="legend-arrow"></i> Односторонняя связь</span> <span><i class="legend-arrow"></i> Односторонняя связь</span>
`; `;
let activeMenu = null; const profileCardCache = new Map();
let centerLogin = state.session.login || ''; let centerLogin = state.session.login || '';
let redrawEdges = () => {}; let redrawEdges = () => {};
let loadSeq = 0; let loadSeq = 0;
function closeNodeMenu() { function profileInfoRoute(login) {
if (!activeMenu) return; const cleanLogin = normalizeLogin(login);
activeMenu.remove(); if (!cleanLogin) return '';
activeMenu = null; if (normKey(cleanLogin) === normKey(state.session.login)) return 'profile-view';
return `user-profile-view/${encodeURIComponent(cleanLogin)}/network-view`;
} }
function openNodeMenu(node, login) { function getProfileCardCached(login) {
closeNodeMenu(); const cleanLogin = normalizeLogin(login);
const menu = document.createElement('div'); const key = normKey(cleanLogin);
menu.className = 'node-menu card'; if (!cleanLogin) return Promise.resolve(null);
menu.innerHTML = ` if (!profileCardCache.has(key)) {
<div class="node-menu-actions"> profileCardCache.set(key, loadUserProfileCard(cleanLogin).catch(() => null));
<button class="ghost-btn" type="button" data-menu-action="show-info">Показать информацию</button> }
<button class="ghost-btn" type="button" data-menu-action="show-graph">Показать связи</button> return profileCardCache.get(key);
</div>
`;
const rect = node.getBoundingClientRect();
const boardRect = board.getBoundingClientRect();
const x = rect.left + rect.width / 2 - boardRect.left;
const y = rect.bottom - boardRect.top + 8;
menu.style.left = `${Math.max(8, Math.min(x - 120, boardRect.width - 248))}px`;
menu.style.top = `${Math.max(8, Math.min(y, boardRect.height - 58))}px`;
const infoBtn = menu.querySelector('[data-menu-action="show-info"]');
const graphBtn = menu.querySelector('[data-menu-action="show-graph"]');
infoBtn?.addEventListener('click', () => {
navigate(`user-profile-view/${encodeURIComponent(login)}/network-view`);
closeNodeMenu();
});
graphBtn?.addEventListener('click', async () => {
closeNodeMenu();
await load(login);
});
board.append(menu);
activeMenu = menu;
} }
function bindNodeInteraction(node, login, onLongPress) { async function hydrateNodeProfiles(layout, nodeElements, requestId) {
let timerId = 0; const uniqueNodes = [];
let startX = 0; const seen = new Set();
let startY = 0; layout.nodes.forEach((nodeModel) => {
let longPressTriggered = false; const key = normKey(nodeModel.login);
if (!key || seen.has(key)) return;
const clearTimer = () => { seen.add(key);
if (!timerId) return; uniqueNodes.push(nodeModel);
window.clearTimeout(timerId);
timerId = 0;
};
node.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
startX = event.clientX;
startY = event.clientY;
longPressTriggered = false;
clearTimer();
timerId = window.setTimeout(async () => {
longPressTriggered = true;
closeNodeMenu();
await onLongPress(login);
}, 500);
}); });
node.addEventListener('pointermove', (event) => { const cards = await Promise.all(uniqueNodes.map((nodeModel) => getProfileCardCached(nodeModel.login)));
if (!timerId) return; if (requestId !== loadSeq) return;
const dx = Math.abs(event.clientX - startX);
const dy = Math.abs(event.clientY - startY); const cardByKey = new Map();
if (dx > 8 || dy > 8) clearTimer(); cards.forEach((card) => {
const login = normalizeLogin(card?.login);
if (!login) return;
cardByKey.set(normKey(login), card);
}); });
node.addEventListener('pointerleave', clearTimer); layout.nodes.forEach((nodeModel) => {
node.addEventListener('pointercancel', clearTimer); const node = nodeElements.get(nodeModel.id);
node.addEventListener('pointerup', (event) => { if (!(node instanceof HTMLElement)) return;
if (event.button !== 0) return; const card = cardByKey.get(normKey(nodeModel.login));
clearTimer(); const cardGender = normalizeGender(card?.gender);
if (longPressTriggered) return; applyNodeText(node, {
openNodeMenu(node, login); login: nodeModel.login,
firstName: card?.firstName || '',
lastName: card?.lastName || '',
role: nodeModel.relation || 'friend',
gender: nodeModel.gender === GENDER_UNKNOWN ? cardGender : nodeModel.gender,
mark: nodeModel.mark || null,
});
});
requestAnimationFrame(() => redrawEdges());
}
function bindNodeInteraction(node, nodeModel) {
node.addEventListener('click', () => {
if (nodeModel.isCenter) {
const routeTo = profileInfoRoute(nodeModel.login);
if (routeTo) navigate(routeTo);
return;
}
void load(nodeModel.login);
}); });
} }
@ -492,7 +588,6 @@ export function render({ navigate }) {
const requestId = ++loadSeq; const requestId = ++loadSeq;
const targetCenter = normalizeLogin(nextCenterLogin || centerLogin || state.session.login); const targetCenter = normalizeLogin(nextCenterLogin || centerLogin || state.session.login);
centerLogin = targetCenter; centerLogin = targetCenter;
closeNodeMenu();
note.textContent = 'Загрузка связей...'; note.textContent = 'Загрузка связей...';
try { try {
@ -513,33 +608,28 @@ export function render({ navigate }) {
login: nodeModel.login, login: nodeModel.login,
kind: nodeModel.kind, kind: nodeModel.kind,
isCenter: nodeModel.isCenter, isCenter: nodeModel.isCenter,
role: nodeModel.relation || 'friend',
gender: nodeModel.gender,
mark: nodeModel.mark, mark: nodeModel.mark,
}); });
node.style.left = `${nodeModel.x}%`; node.style.left = `${nodeModel.x}%`;
node.style.top = `${nodeModel.y}%`; node.style.top = `${nodeModel.y}%`;
board.append(node); board.append(node);
nodeElements.set(nodeModel.id, node); nodeElements.set(nodeModel.id, node);
if (!nodeModel.isCenter) bindNodeInteraction(node, nodeModel.login, load); bindNodeInteraction(node, nodeModel);
}); });
redrawEdges = () => renderEdges(svg, board, nodeElements, layout.edges); redrawEdges = () => renderEdges(svg, board, nodeElements, layout.edges);
requestAnimationFrame(() => redrawEdges()); requestAnimationFrame(() => redrawEdges());
void hydrateNodeProfiles(layout, nodeElements, requestId);
note.textContent = 'Тап по узлу: меню «Показать информацию» или «Показать связи». Долгое нажатие: сделать узел центром.'; note.textContent = 'Тап по узлу: нецентральный узел станет центром. Тап по центральному узлу: открыть профиль.';
} catch (error) { } catch (error) {
if (requestId !== loadSeq) return; if (requestId !== loadSeq) return;
note.textContent = `Ошибка загрузки связей: ${error?.message || 'unknown'}`; note.textContent = `Ошибка загрузки связей: ${error?.message || 'unknown'}`;
} }
} }
const outsideTapHandler = (event) => {
if (!activeMenu) return;
if (!(event.target instanceof Node)) return;
if (activeMenu.contains(event.target)) return;
closeNodeMenu();
};
document.addEventListener('pointerdown', outsideTapHandler, true);
const onResize = () => redrawEdges(); const onResize = () => redrawEdges();
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
@ -550,19 +640,10 @@ export function render({ navigate }) {
} }
screen.cleanup = () => { screen.cleanup = () => {
document.removeEventListener('pointerdown', outsideTapHandler, true);
window.removeEventListener('resize', onResize); window.removeEventListener('resize', onResize);
if (observer) observer.disconnect(); if (observer) observer.disconnect();
}; };
board.addEventListener('pointerdown', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest('.node')) return;
if (target.closest('.node-menu')) return;
closeNodeMenu();
});
load(); load();
screen.append(renderHeader({ title: 'Связи' }), legend, board, note); screen.append(renderHeader({ title: 'Связи' }), legend, board, note);
return screen; return screen;

View File

@ -41,6 +41,7 @@ const GENDER_OPTIONS = Object.freeze([
const RELATIVE_RELATION_OPTIONS = Object.freeze([ const RELATIVE_RELATION_OPTIONS = Object.freeze([
{ value: 'parent', label: 'Родитель (мать/отец по полу)' }, { value: 'parent', label: 'Родитель (мать/отец по полу)' },
{ value: 'child', label: 'Ребёнок (сын/дочь по полу)' }, { value: 'child', label: 'Ребёнок (сын/дочь по полу)' },
{ value: 'spouse', label: 'Жена / Муж (по полу)' },
{ value: 'sibling', label: 'Брат или сестра (по полу)' }, { value: 'sibling', label: 'Брат или сестра (по полу)' },
{ value: 'close_friend', label: 'Близкий друг' }, { value: 'close_friend', label: 'Близкий друг' },
]); ]);
@ -69,6 +70,11 @@ function relationAccusativeLabel(type, targetGender) {
if (gender === PROFILE_GENDER_FEMALE) return 'сестру'; if (gender === PROFILE_GENDER_FEMALE) return 'сестру';
return 'брата/сестру'; return 'брата/сестру';
} }
if (type === 'spouse') {
if (gender === PROFILE_GENDER_MALE) return 'мужа';
if (gender === PROFILE_GENDER_FEMALE) return 'жену';
return 'жену/мужа';
}
return 'близкого друга'; return 'близкого друга';
} }
@ -134,7 +140,7 @@ export function render({ navigate }) {
relativesCard.innerHTML = ` relativesCard.innerHTML = `
<div class="profile-param-value"><b>Близкие родственники</b></div> <div class="profile-param-value"><b>Близкие родственники</b></div>
<div class="meta-muted"> <div class="meta-muted">
Добавьте связь: родитель, ребёнок, брат/сестра или близкий друг. Добавьте связь: родитель, ребёнок, жена/муж, брат/сестра или близкий друг.
Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя. Формулировка (мать/отец, брат/сестра) определяется по полу выбранного пользователя.
</div> </div>
<button class="secondary-btn" type="button" data-add-relative="true">Добавить близких родственников</button> <button class="secondary-btn" type="button" data-add-relative="true">Добавить близких родственников</button>

View File

@ -50,6 +50,7 @@ const CONNECTION_SUBTYPES = Object.freeze({
friend: { on: 10, off: 11 }, friend: { on: 10, off: 11 },
contact: { on: 20, off: 21 }, contact: { on: 20, off: 21 },
follow: { on: 30, off: 31 }, follow: { on: 30, off: 31 },
spouse: { on: 40, off: 41 },
parent: { on: 50, off: 51 }, parent: { on: 50, off: 51 },
child: { on: 52, off: 53 }, child: { on: 52, off: 53 },
sibling: { on: 54, off: 55 }, sibling: { on: 54, off: 55 },

View File

@ -1388,7 +1388,7 @@ textarea.input {
.node { .node {
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 90px; width: 126px;
border: 0; border: 0;
background: transparent; background: transparent;
padding: 0; padding: 0;
@ -1464,9 +1464,49 @@ textarea.input {
} }
.node-label { .node-label {
font-size: 11px; display: grid;
gap: 1px;
margin-top: 1px;
font-size: 10px;
color: #d6e2ff; color: #d6e2ff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.28); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.28);
line-height: 1.12;
}
.node-label.is-login-only .node-name {
display: none;
}
.node-name {
display: grid;
gap: 1px;
color: #f2f6ff;
}
.node-name-line {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-login {
display: block;
color: #c8dafd;
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-relation {
display: block;
color: #ffd5b3;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.node:focus-visible .node-dot, .node:focus-visible .node-dot,

View File

@ -93,6 +93,11 @@ public final class MsgSubType {
/** Отписаться (unfollow). */ /** Отписаться (unfollow). */
public static final short CONNECTION_UNFOLLOW = 31; public static final short CONNECTION_UNFOLLOW = 31;
/** Добавить связь "жена/муж". */
public static final short CONNECTION_SPOUSE = 40;
/** Удалить связь "жена/муж". */
public static final short CONNECTION_UNSPOUSE = 41;
/** Добавить связь "родитель". */ /** Добавить связь "родитель". */
public static final short CONNECTION_PARENT = 50; public static final short CONNECTION_PARENT = 50;
/** Удалить связь "родитель". */ /** Удалить связь "родитель". */

View File

@ -185,6 +185,8 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF) || v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF) || v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF) || v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF)
|| v == (MsgSubType.CONNECTION_SPOUSE & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNSPOUSE & 0xFFFF)
|| v == (MsgSubType.CONNECTION_PARENT & 0xFFFF) || v == (MsgSubType.CONNECTION_PARENT & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNPARENT & 0xFFFF) || v == (MsgSubType.CONNECTION_UNPARENT & 0xFFFF)
|| v == (MsgSubType.CONNECTION_CHILD & 0xFFFF) || v == (MsgSubType.CONNECTION_CHILD & 0xFFFF)

View File

@ -54,6 +54,9 @@ public final class DatabaseInitializer {
public static final short CONNECTION_FOLLOW = 30; public static final short CONNECTION_FOLLOW = 30;
public static final short CONNECTION_UNFOLLOW = 31; public static final short CONNECTION_UNFOLLOW = 31;
public static final short CONNECTION_SPOUSE = 40;
public static final short CONNECTION_UNSPOUSE = 41;
public static final short CONNECTION_PARENT = 50; public static final short CONNECTION_PARENT = 50;
public static final short CONNECTION_UNPARENT = 51; public static final short CONNECTION_UNPARENT = 51;

View File

@ -194,6 +194,7 @@ public final class DatabaseTriggersInstaller {
int FRIEND = (int) DatabaseInitializer.CONNECTION_FRIEND; int FRIEND = (int) DatabaseInitializer.CONNECTION_FRIEND;
int CONTACT = (int) DatabaseInitializer.CONNECTION_CONTACT; int CONTACT = (int) DatabaseInitializer.CONNECTION_CONTACT;
int FOLLOW = (int) DatabaseInitializer.CONNECTION_FOLLOW; int FOLLOW = (int) DatabaseInitializer.CONNECTION_FOLLOW;
int SPOUSE = (int) DatabaseInitializer.CONNECTION_SPOUSE;
int PARENT = (int) DatabaseInitializer.CONNECTION_PARENT; int PARENT = (int) DatabaseInitializer.CONNECTION_PARENT;
int CHILD = (int) DatabaseInitializer.CONNECTION_CHILD; int CHILD = (int) DatabaseInitializer.CONNECTION_CHILD;
int SIBLING = (int) DatabaseInitializer.CONNECTION_SIBLING; int SIBLING = (int) DatabaseInitializer.CONNECTION_SIBLING;
@ -201,6 +202,7 @@ public final class DatabaseTriggersInstaller {
int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND; int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND;
int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT; int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT;
int UNFOLLOW = (int) DatabaseInitializer.CONNECTION_UNFOLLOW; int UNFOLLOW = (int) DatabaseInitializer.CONNECTION_UNFOLLOW;
int UNSPOUSE = (int) DatabaseInitializer.CONNECTION_UNSPOUSE;
int UNPARENT = (int) DatabaseInitializer.CONNECTION_UNPARENT; int UNPARENT = (int) DatabaseInitializer.CONNECTION_UNPARENT;
int UNCHILD = (int) DatabaseInitializer.CONNECTION_UNCHILD; int UNCHILD = (int) DatabaseInitializer.CONNECTION_UNCHILD;
int UNSIBLING = (int) DatabaseInitializer.CONNECTION_UNSIBLING; int UNSIBLING = (int) DatabaseInitializer.CONNECTION_UNSIBLING;
@ -210,7 +212,7 @@ public final class DatabaseTriggersInstaller {
AFTER INSERT ON blocks AFTER INSERT ON blocks
WHEN NEW.msg_type = 3 WHEN NEW.msg_type = 3
BEGIN BEGIN
-- FRIEND/CONTACT/FOLLOW/PARENT/CHILD/SIBLING: -- FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING:
-- 1) если записи РЅРµС вЂ СЃРѕР·РґР°СРј -- 1) если записи РЅРµС вЂ СЃРѕР·РґР°СРј
INSERT OR IGNORE INTO connections_state ( INSERT OR IGNORE INTO connections_state (
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
@ -231,7 +233,7 @@ public final class DatabaseTriggersInstaller {
NEW.to_bch_name, NEW.to_bch_name,
NEW.to_block_number, NEW.to_block_number,
NEW.to_block_hash NEW.to_block_hash
WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d) WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d)
AND COALESCE( AND COALESCE(
NEW.to_login, NEW.to_login,
CASE CASE
@ -262,7 +264,7 @@ public final class DatabaseTriggersInstaller {
ELSE NULL ELSE NULL
END END
) )
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d) AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d)
AND COALESCE( AND COALESCE(
NEW.to_login, NEW.to_login,
CASE CASE
@ -275,7 +277,7 @@ public final class DatabaseTriggersInstaller {
) IS NOT NULL ) IS NOT NULL
AND NEW.to_bch_name IS NOT NULL; AND NEW.to_bch_name IS NOT NULL;
-- UNFRIEND/UNCONTACT/UNFOLLOW/UNPARENT/UNCHILD/UNSIBLING: -- UNFRIEND/UNCONTACT/UNFOLLOW/UNSPOUSE/UNPARENT/UNCHILD/UNSIBLING:
-- удаляем СЃРѕРѕСРІРµССЃСРІСѓСЋСее "позитивное" СЃРѕСЃСРѕСЏРЅРёРµ -- удаляем СЃРѕРѕСРІРµССЃСРІСѓСЋСее "позитивное" СЃРѕСЃСРѕСЏРЅРёРµ
DELETE FROM connections_state DELETE FROM connections_state
WHERE login = NEW.login WHERE login = NEW.login
@ -296,6 +298,7 @@ public final class DatabaseTriggersInstaller {
WHEN %d THEN %d WHEN %d THEN %d
WHEN %d THEN %d WHEN %d THEN %d
WHEN %d THEN %d WHEN %d THEN %d
WHEN %d THEN %d
ELSE rel_type ELSE rel_type
END END
AND COALESCE( AND COALESCE(
@ -308,20 +311,21 @@ public final class DatabaseTriggersInstaller {
ELSE NULL ELSE NULL
END END
) IS NOT NULL ) IS NOT NULL
AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d); AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d);
END; END;
""".formatted( """.formatted(
FRIEND, CONTACT, FOLLOW, PARENT, CHILD, SIBLING, FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING,
FRIEND, CONTACT, PARENT, CHILD, SIBLING, FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING,
UNFRIEND, FRIEND, UNFRIEND, FRIEND,
UNCONTACT, CONTACT, UNCONTACT, CONTACT,
UNFOLLOW, FOLLOW, UNFOLLOW, FOLLOW,
UNSPOUSE, SPOUSE,
UNPARENT, PARENT, UNPARENT, PARENT,
UNCHILD, CHILD, UNCHILD, CHILD,
UNSIBLING, SIBLING, UNSIBLING, SIBLING,
UNFRIEND, UNCONTACT, UNFOLLOW, UNPARENT, UNCHILD, UNSIBLING UNFRIEND, UNCONTACT, UNFOLLOW, UNSPOUSE, UNPARENT, UNCHILD, UNSIBLING
)); ));
} }

View File

@ -39,9 +39,9 @@ public final class MsgSubType {
/* ===================== CONNECTION (msg_type=3) ===================== */ /* ===================== CONNECTION (msg_type=3) ===================== */
/** /**
* Совпадает с ConnectionBody: * Совпадает с ConnectionBody:
* SET: CLOSE_FRIEND(=FRIEND)=10, CONTACT=20, FOLLOW=30, PARENT=50, CHILD=52, SIBLING=54 * SET: CLOSE_FRIEND(=FRIEND)=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54
* UNSET: UNCLOSE_FRIEND(=UNFRIEND)=11, UNCONTACT=21, UNFOLLOW=31, UNPARENT=51, UNCHILD=53, UNSIBLING=55 * UNSET: UNCLOSE_FRIEND(=UNFRIEND)=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55
*/ */
/** Добавить в близкие друзья (close friend). */ /** Добавить в близкие друзья (close friend). */
@ -68,6 +68,12 @@ public final class MsgSubType {
/** Отписаться (unfollow). */ /** Отписаться (unfollow). */
public static final short CONNECTION_UNFOLLOW = 31; public static final short CONNECTION_UNFOLLOW = 31;
/** Добавить связь "жена/муж". */
public static final short CONNECTION_SPOUSE = 40;
/** Удалить связь "жена/муж". */
public static final short CONNECTION_UNSPOUSE = 41;
/** Добавить связь "родитель". */ /** Добавить связь "родитель". */
public static final short CONNECTION_PARENT = 50; public static final short CONNECTION_PARENT = 50;
@ -92,8 +98,6 @@ public final class MsgSubType {
public static final short USER_PARAM_TEXT_TEXT = 1; public static final short USER_PARAM_TEXT_TEXT = 1;
/* ===================== РЕЗЕРВ НА БУДУЩЕЕ ===================== */ /* ===================== РЕЗЕРВ НА БУДУЩЕЕ ===================== */
// Если позже захочешь BLOCK/UNBLOCK лучше добавить НОВЫЕ значения, // Если позже захочешь BLOCK/UNBLOCK лучше добавить новые значения,
// не трогая 10/20/30 и 11/21/31 (например, 40/41). // не трогая уже занятые коды.
// public static final short CONNECTION_BLOCK = 40;
// public static final short CONNECTION_UNBLOCK = 41;
} }

View File

@ -46,6 +46,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
List<String> inContacts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT); List<String> inContacts = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CONTACT);
List<String> outFollows = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW); List<String> outFollows = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW);
List<String> inFollows = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW); List<String> inFollows = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_FOLLOW);
List<String> outSpouses = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SPOUSE);
List<String> inSpouses = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SPOUSE);
List<String> outParents = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_PARENT); List<String> outParents = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_PARENT);
List<String> inParents = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_PARENT); List<String> inParents = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_PARENT);
List<String> outChildren = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD); List<String> outChildren = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD);
@ -56,9 +58,10 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
LinkedHashSet<String> allLogins = new LinkedHashSet<>(); LinkedHashSet<String> allLogins = new LinkedHashSet<>();
allLogins.add(canonicalLogin); allLogins.add(canonicalLogin);
addAllLogins(allLogins, outFriends, inFriends, outContacts, inContacts, outFollows, inFollows, addAllLogins(allLogins, outFriends, inFriends, outContacts, inContacts, outFollows, inFollows,
outParents, inParents, outChildren, inChildren, outSiblings, inSiblings); outSpouses, inSpouses, outParents, inParents, outChildren, inChildren, outSiblings, inSiblings);
Map<String, UserMeta> metaByLogin = loadUserMeta(c, allLogins); Map<String, UserMeta> metaByLogin = loadUserMeta(c, allLogins);
List<String> spouseLogins = mergeUnique(outSpouses, inSpouses);
List<String> parentLogins = mergeUnique(outParents, inChildren); List<String> parentLogins = mergeUnique(outParents, inChildren);
List<String> childLogins = mergeUnique(outChildren, inParents); List<String> childLogins = mergeUnique(outChildren, inParents);
List<String> siblingLogins = mergeUnique(outSiblings, inSiblings); List<String> siblingLogins = mergeUnique(outSiblings, inSiblings);
@ -74,6 +77,8 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
resp.setInContacts(inContacts); resp.setInContacts(inContacts);
resp.setOutFollows(outFollows); resp.setOutFollows(outFollows);
resp.setInFollows(inFollows); resp.setInFollows(inFollows);
resp.setOutSpouses(outSpouses);
resp.setInSpouses(inSpouses);
resp.setOutParents(outParents); resp.setOutParents(outParents);
resp.setInParents(inParents); resp.setInParents(inParents);
resp.setOutChildren(outChildren); resp.setOutChildren(outChildren);
@ -83,6 +88,7 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
resp.setParents(toRelativeItems(parentLogins, metaByLogin)); resp.setParents(toRelativeItems(parentLogins, metaByLogin));
resp.setChildren(toRelativeItems(childLogins, metaByLogin)); resp.setChildren(toRelativeItems(childLogins, metaByLogin));
resp.setSiblings(toRelativeItems(siblingLogins, metaByLogin)); resp.setSiblings(toRelativeItems(siblingLogins, metaByLogin));
resp.setSpouses(toRelativeItems(spouseLogins, metaByLogin));
resp.setAllUsers(toUserMarkItems(allLogins, metaByLogin)); resp.setAllUsers(toUserMarkItems(allLogins, metaByLogin));
return resp; return resp;
} }

View File

@ -13,6 +13,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
private List<String> inContacts = new ArrayList<>(); private List<String> inContacts = new ArrayList<>();
private List<String> outFollows = new ArrayList<>(); private List<String> outFollows = new ArrayList<>();
private List<String> inFollows = new ArrayList<>(); private List<String> inFollows = new ArrayList<>();
private List<String> outSpouses = new ArrayList<>();
private List<String> inSpouses = new ArrayList<>();
private List<String> outParents = new ArrayList<>(); private List<String> outParents = new ArrayList<>();
private List<String> inParents = new ArrayList<>(); private List<String> inParents = new ArrayList<>();
private List<String> outChildren = new ArrayList<>(); private List<String> outChildren = new ArrayList<>();
@ -22,6 +24,7 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
private List<RelativeItem> parents = new ArrayList<>(); private List<RelativeItem> parents = new ArrayList<>();
private List<RelativeItem> children = new ArrayList<>(); private List<RelativeItem> children = new ArrayList<>();
private List<RelativeItem> siblings = new ArrayList<>(); private List<RelativeItem> siblings = new ArrayList<>();
private List<RelativeItem> spouses = new ArrayList<>();
private List<UserMarkItem> allUsers = new ArrayList<>(); private List<UserMarkItem> allUsers = new ArrayList<>();
public static class AvatarItem { public static class AvatarItem {
@ -83,6 +86,10 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
public void setOutFollows(List<String> outFollows) { this.outFollows = outFollows; } public void setOutFollows(List<String> outFollows) { this.outFollows = outFollows; }
public List<String> getInFollows() { return inFollows; } public List<String> getInFollows() { return inFollows; }
public void setInFollows(List<String> inFollows) { this.inFollows = inFollows; } public void setInFollows(List<String> inFollows) { this.inFollows = inFollows; }
public List<String> getOutSpouses() { return outSpouses; }
public void setOutSpouses(List<String> outSpouses) { this.outSpouses = outSpouses; }
public List<String> getInSpouses() { return inSpouses; }
public void setInSpouses(List<String> inSpouses) { this.inSpouses = inSpouses; }
public List<String> getOutParents() { return outParents; } public List<String> getOutParents() { return outParents; }
public void setOutParents(List<String> outParents) { this.outParents = outParents; } public void setOutParents(List<String> outParents) { this.outParents = outParents; }
public List<String> getInParents() { return inParents; } public List<String> getInParents() { return inParents; }
@ -101,6 +108,8 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
public void setChildren(List<RelativeItem> children) { this.children = children; } public void setChildren(List<RelativeItem> children) { this.children = children; }
public List<RelativeItem> getSiblings() { return siblings; } public List<RelativeItem> getSiblings() { return siblings; }
public void setSiblings(List<RelativeItem> siblings) { this.siblings = siblings; } public void setSiblings(List<RelativeItem> siblings) { this.siblings = siblings; }
public List<RelativeItem> getSpouses() { return spouses; }
public void setSpouses(List<RelativeItem> spouses) { this.spouses = spouses; }
public List<UserMarkItem> getAllUsers() { return allUsers; } public List<UserMarkItem> getAllUsers() { return allUsers; }
public void setAllUsers(List<UserMarkItem> allUsers) { this.allUsers = allUsers; } public void setAllUsers(List<UserMarkItem> allUsers) { this.allUsers = allUsers; }
} }