diff --git a/AGENTS.md b/AGENTS.md
index 089bcf0..417aa37 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -15,6 +15,14 @@
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
+- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
+- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
+
+## Документация личных сообщений (DM)
+- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/README.md`.
+- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять `Dev_Docs/Personal_Messages/README.md`.
+- Логика личных сообщений в коде должна всегда соответствовать `Dev_Docs/Personal_Messages/README.md`.
+- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
## Версионирование
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория).
diff --git a/Dev_Docs/API/04_Add_Block_to_Blockchain_API.md b/Dev_Docs/API/04_Add_Block_to_Blockchain_API.md
index 3447eeb..5cf882f 100644
--- a/Dev_Docs/API/04_Add_Block_to_Blockchain_API.md
+++ b/Dev_Docs/API/04_Add_Block_to_Blockchain_API.md
@@ -107,6 +107,20 @@
- `CONNECTION_UNCONTACT (21)`
- `CONNECTION_FOLLOW (30)`
- `CONNECTION_UNFOLLOW (31)`
+ - `CONNECTION_SPOUSE (40)`
+ - `CONNECTION_UNSPOUSE (41)`
+ - `CONNECTION_PARENT (50)`
+ - `CONNECTION_UNPARENT (51)`
+ - `CONNECTION_CHILD (52)`
+ - `CONNECTION_UNCHILD (53)`
+ - `CONNECTION_SIBLING (54)`
+ - `CONNECTION_UNSIBLING (55)`
+ - `CONNECTION_KNOWN_PERSON (60)`
+ - `CONNECTION_UNKNOWN_PERSON (61)`
+ - `CONNECTION_SHINE_CONFIRMED (70)`
+ - `CONNECTION_SHINE_UNCONFIRMED (71)`
+ - `CONNECTION_SHINE_SEEN (74)`
+ - `CONNECTION_SHINE_UNSEEN (75)`
5. **USER_PARAM (type=4)**
- `USER_PARAM_TEXT_TEXT (1)`
diff --git a/Dev_Docs/Blockchain/13_CONNECTION_Blocks.md b/Dev_Docs/Blockchain/13_CONNECTION_Blocks.md
index bbfe25a..b2991b5 100644
--- a/Dev_Docs/Blockchain/13_CONNECTION_Blocks.md
+++ b/Dev_Docs/Blockchain/13_CONNECTION_Blocks.md
@@ -4,20 +4,16 @@ CONNECTION-тип описывает социальные связи и подп
## Подтипы
-1. `subType=10` — `CONNECTION_FRIEND`
-2. `subType=11` — `CONNECTION_UNFRIEND`
-3. `subType=20` — `CONNECTION_CONTACT`
-4. `subType=21` — `CONNECTION_UNCONTACT`
-5. `subType=30` — `CONNECTION_FOLLOW`
-6. `subType=31` — `CONNECTION_UNFOLLOW`
-7. `subType=40` — `CONNECTION_SPOUSE`
-8. `subType=41` — `CONNECTION_UNSPOUSE`
-9. `subType=50` — `CONNECTION_PARENT`
-10. `subType=51` — `CONNECTION_UNPARENT`
-11. `subType=52` — `CONNECTION_CHILD`
-12. `subType=53` — `CONNECTION_UNCHILD`
-13. `subType=54` — `CONNECTION_SIBLING`
-14. `subType=55` — `CONNECTION_UNSIBLING`
+• `10/11` — `close_friend / unclose_friend` (близкий друг)
+• `20/21` — `contact / uncontact` (контакт)
+• `30/31` — `follow / unfollow` (подписан)
+• `40/41` — `spouse / unspouse` (супруг/супруга)
+• `50/51` — `parent / unparent` (родитель)
+• `52/53` — `child / unchild` (ребёнок)
+• `54/55` — `sibling / unsibling` (брат/сестра)
+• `60/61` — `known_person / unknown_person` (знаю этого человека)
+• `70/71` — `shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий)
+• `74/75` — `shine_seen / shine_unseen` (мало знаком, но видел сияющим)
## Общий формат payload
diff --git a/Dev_Docs/Blockchain/CHANGELOG.md b/Dev_Docs/Blockchain/CHANGELOG.md
index c168468..ce58f2e 100644
--- a/Dev_Docs/Blockchain/CHANGELOG.md
+++ b/Dev_Docs/Blockchain/CHANGELOG.md
@@ -1,5 +1,13 @@
# История изменений документации блокчейна
+## 2026-05-20 11:34:17 +0300
+- Базовый коммит-ориентир: `a53444b`.
+- В `13_CONNECTION_Blocks.md` добавлены новые CONNECTION подтипы:
+ - `60/61` — `known_person / unknown_person` (знаю этого человека);
+ - `70/71` — `shine_confirmed / shine_unconfirmed` (точно уверен, что сияющий);
+ - `74/75` — `shine_seen / shine_unseen` (мало знаком, но видел сияющим).
+- Обновлён список CONNECTION-подтипов в `Dev_Docs/API/04_Add_Block_to_Blockchain_API.md`.
+
## 2026-05-19 20:30:21 +0300
- Базовый коммит-ориентир: `7986184`.
- Уточнён документ `11_TEXT_Blocks.md`: для `TEXT_EDIT_POST` и `TEXT_EDIT_REPLY` зафиксировано, что `textLen=0` допустим и трактуется как логическое удаление сообщения.
diff --git a/Dev_Docs/Pending_Features/2026-05-20_1134_connection-shine-known-codes.md b/Dev_Docs/Pending_Features/2026-05-20_1134_connection-shine-known-codes.md
new file mode 100644
index 0000000..3124883
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-05-20_1134_connection-shine-known-codes.md
@@ -0,0 +1,23 @@
+## Краткое описание
+Добавлены новые типы connection-связей в блокчейне и API:
+- `known_person` (`60/61`)
+- `shine_confirmed` (`70/71`)
+- `shine_seen` (`74/75`)
+
+## Что проверять
+1. `AddBlock` принимает новые `msg_sub_type` для `type=3`.
+2. Связи корректно попадают в `connections_state`:
+ - ON создаёт/обновляет запись;
+ - OFF удаляет запись соответствующего ON-типа.
+3. `GetUserConnectionsGraph` возвращает новые поля:
+ - `outKnownPersons`, `inKnownPersons`
+ - `outShineConfirmed`, `inShineConfirmed`
+ - `outShineSeen`, `inShineSeen`
+4. Клиент `setUserRelation` принимает `kind`:
+ - `known_person`, `shine_confirmed`, `shine_seen`.
+
+## Ожидаемый результат
+Новые связи работают как обычные ON/OFF relation-типы, но не ломают текущие friend/contact/follow и остальные существующие связи.
+
+## Статус
+`pending`
diff --git a/Dev_Docs/Pending_Features/2026-05-20_1221_profile-opinion-ui-and-order.md b/Dev_Docs/Pending_Features/2026-05-20_1221_profile-opinion-ui-and-order.md
new file mode 100644
index 0000000..9651779
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-05-20_1221_profile-opinion-ui-and-order.md
@@ -0,0 +1,25 @@
+## Краткое описание
+Перестроен блок связей в профиле чужого пользователя и добавлен UI для одностороннего "мнения" (`known_person` / `shine_confirmed` / `shine_seen`) с взаимным исключением на уровне UI.
+
+## Что проверять
+1. Порядок базовых строк в профиле:
+ - Контакт
+ - Близкий друг
+ - Подписка
+2. Под этими строками отображается блок мнений:
+ - при отсутствии мнения кнопка `Добавить связь`;
+ - при наличии мнения кнопка `Изменить связи`;
+ - показываются текстовые формулировки для активного мнения.
+3. В модальном меню:
+ - варианты добавления (синие);
+ - `Убрать мнение` (красная).
+4. При смене мнения отправляется последовательность:
+ - OFF старой связи,
+ - ON новой связи.
+5. Для новых мнений показываются только исходящие (`out*`) оценки текущего пользователя (односторонняя логика).
+
+## Ожидаемый результат
+Пользователь управляет одним активным мнением через UI, состояние читается корректно и не ломает существующие friend/contact/follow кнопки.
+
+## Статус
+`pending`
diff --git a/VERSION.properties b/VERSION.properties
index 5e43199..e40f564 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.78
-server.version=1.2.72
+client.version=1.2.79
+server.version=1.2.73
diff --git a/shine-UI/js/pages/user-profile-view.js b/shine-UI/js/pages/user-profile-view.js
index a68b1b8..818f9dc 100644
--- a/shine-UI/js/pages/user-profile-view.js
+++ b/shine-UI/js/pages/user-profile-view.js
@@ -28,28 +28,28 @@ function genderText(value) {
}
function relationButtonLabel(kind, flags) {
- if (kind === 'follow') return flags.outFollow ? 'Отписаться' : 'Подписаться';
+ if (kind === 'contact') return flags.outContact ? 'Убрать из контактов' : 'Добавить в контакты';
if (kind === 'friend') return flags.outFriend ? 'Убрать из близких друзей' : 'Добавить в близкие друзья';
- return flags.outContact ? 'Убрать из контактов' : 'Добавить в контакты';
+ return flags.outFollow ? 'Отписаться' : 'Подписаться';
}
function relationNextState(kind, flags) {
- if (kind === 'follow') return !flags.outFollow;
+ if (kind === 'contact') return !flags.outContact;
if (kind === 'friend') return !flags.outFriend;
- return !flags.outContact;
+ return !flags.outFollow;
}
function relationConfirmLabel(kind) {
- if (kind === 'follow') return 'подписку';
+ if (kind === 'contact') return 'контакт';
if (kind === 'friend') return 'статус близкого друга';
- return 'контакт';
+ return 'подписку';
}
function relationStateText(kind, flags) {
- if (kind === 'follow') {
- if (flags.outFollow && flags.inFollow) return 'Вы взаимно подписаны.';
- if (flags.outFollow) return 'Вы подписаны на этот профиль.';
- if (flags.inFollow) return 'Этот профиль подписан на вас.';
+ if (kind === 'contact') {
+ if (flags.outContact && flags.inContact) return 'Вы обменялись контактами.';
+ if (flags.outContact) return 'Вы добавили этот профиль в контакты.';
+ if (flags.inContact) return 'Этот профиль добавил вас в контакты.';
return '';
}
if (kind === 'friend') {
@@ -58,12 +58,52 @@ function relationStateText(kind, flags) {
if (flags.inFriend) return 'Этот профиль считает вас близким другом.';
return '';
}
- if (flags.outContact && flags.inContact) return 'Вы обменялись контактами.';
- if (flags.outContact) return 'Вы добавили этот профиль в контакты.';
- if (flags.inContact) return 'Этот профиль добавил вас в контакты.';
+ if (flags.outFollow && flags.inFollow) return 'Вы взаимно подписаны.';
+ if (flags.outFollow) return 'Вы подписаны на этот профиль.';
+ if (flags.inFollow) return 'Этот профиль подписан на вас.';
return '';
}
+function opinionItemsFromFlags(flags) {
+ const items = [];
+ if (flags.outShineSeen) {
+ items.push({
+ kind: 'shine_seen',
+ text: 'вы утверждаете, что очень мало знаете этого человека, но вы видели его сияющим, и всё, что вы о нём знаете, подтверждает это',
+ label: 'видел сияющим',
+ });
+ }
+ if (flags.outShineConfirmed) {
+ items.push({
+ kind: 'shine_confirmed',
+ text: 'вы утверждаете, что достаточно хорошо знаете этого человека и точно уверены, что этот человек сияющий',
+ label: 'точно сияющий',
+ });
+ }
+ if (flags.outKnownPerson) {
+ items.push({
+ kind: 'known_person',
+ text: 'вы утверждаете, что просто знаете этого человека',
+ label: 'просто знаю',
+ });
+ }
+ return items;
+}
+
+function resolveActiveOpinionKind(flags) {
+ if (flags.outShineSeen) return 'shine_seen';
+ if (flags.outShineConfirmed) return 'shine_confirmed';
+ if (flags.outKnownPerson) return 'known_person';
+ return '';
+}
+
+function opinionLabelByKind(kind) {
+ if (kind === 'shine_seen') return 'мало знаком, но видел сияющим';
+ if (kind === 'shine_confirmed') return 'точно уверен, что сияющий';
+ if (kind === 'known_person') return 'просто знаю человека';
+ return kind;
+}
+
function renderIdentity(card) {
const lines = buildIdentityLines({
login: card.login,
@@ -109,10 +149,12 @@ function renderReadOnlyBadges(card) {
function renderRelations(flags) {
const rows = [
- { kind: 'follow', text: relationStateText('follow', flags), button: relationButtonLabel('follow', flags) },
- { kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) },
{ kind: 'contact', text: relationStateText('contact', flags), button: relationButtonLabel('contact', flags) },
+ { kind: 'friend', text: relationStateText('friend', flags), button: relationButtonLabel('friend', flags) },
+ { kind: 'follow', text: relationStateText('follow', flags), button: relationButtonLabel('follow', flags) },
];
+ const opinionItems = opinionItemsFromFlags(flags);
+ const hasOpinion = opinionItems.length > 0;
return `
@@ -122,10 +164,65 @@ function renderRelations(flags) {
`).join('')}
+
+
+ ${opinionItems.map((item) => `
+
${escapeHtml(item.text)}
+ `).join('')}
+
+
Добавьте одну из этих трёх формулировок.
+
+
+ ${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'}
+
+
`;
}
+function openOpinionMenuModal({ flags, onApply }) {
+ const root = document.getElementById('modal-root');
+ if (!root) return;
+ const activeKind = resolveActiveOpinionKind(flags);
+ const items = [
+ { kind: 'known_person', title: 'просто знаю человека' },
+ { kind: 'shine_confirmed', title: 'точно уверен, что сияющий' },
+ { kind: 'shine_seen', title: 'мало знаком, но видел сияющим' },
+ ];
+ const rowsHtml = items
+ .filter((item) => item.kind !== activeKind)
+ .map((item) => ``)
+ .join('');
+ const removeHtml = activeKind
+ ? ``
+ : '';
+
+ root.innerHTML = `
+
+
+
${activeKind ? 'Изменить связи' : 'Добавить связь'}
+
${rowsHtml}${removeHtml}
+
+
+
+ `;
+
+ const close = () => { root.innerHTML = ''; };
+ root.querySelector('#user-opinion-modal-close')?.addEventListener('click', close);
+ root.querySelector('#user-opinion-modal')?.addEventListener('click', (event) => {
+ if (event.target?.id === 'user-opinion-modal') close();
+ });
+ root.querySelectorAll('[data-opinion-mode]').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const nextKind = String(btn.getAttribute('data-opinion-kind') || '').trim();
+ const mode = String(btn.getAttribute('data-opinion-mode') || '').trim();
+ close();
+ if (!nextKind) return;
+ await onApply({ mode, nextKind, activeKind });
+ });
+ });
+}
+
function renderReadOnlyParams(card) {
const rows = [
{ label: 'Имя', value: card.firstName },
@@ -179,14 +276,17 @@ export function render({ navigate, route }) {
const followBtn = body.querySelector('[data-relation-action="follow"]');
const friendBtn = body.querySelector('[data-relation-action="friend"]');
const contactBtn = body.querySelector('[data-relation-action="contact"]');
- if (!followBtn || !friendBtn || !contactBtn || !currentFlags) return;
+ const opinionBtn = body.querySelector('[data-relation-action="opinion-menu"]');
+ if (!followBtn || !friendBtn || !contactBtn || !opinionBtn || !currentFlags) return;
const isSelf = currentCard && currentCard.login.toLowerCase() === sessionLogin.toLowerCase();
- followBtn.textContent = relationButtonLabel('follow', currentFlags);
- friendBtn.textContent = relationButtonLabel('friend', currentFlags);
contactBtn.textContent = relationButtonLabel('contact', currentFlags);
- followBtn.disabled = Boolean(isSelf);
- friendBtn.disabled = Boolean(isSelf);
+ friendBtn.textContent = relationButtonLabel('friend', currentFlags);
+ followBtn.textContent = relationButtonLabel('follow', currentFlags);
contactBtn.disabled = Boolean(isSelf);
+ friendBtn.disabled = Boolean(isSelf);
+ followBtn.disabled = Boolean(isSelf);
+ opinionBtn.textContent = opinionItemsFromFlags(currentFlags).length ? 'Изменить связи' : 'Добавить связь';
+ opinionBtn.disabled = Boolean(isSelf);
}
async function refresh() {
@@ -243,6 +343,14 @@ export function render({ navigate, route }) {
return;
}
+ if (kind === 'opinion-menu') {
+ openOpinionMenuModal({
+ flags: currentFlags,
+ onApply: onOpinionApply,
+ });
+ return;
+ }
+
const nextEnabled = relationNextState(kind, currentFlags);
const confirmed = window.confirm(
`Изменить ${relationConfirmLabel(kind)} с пользователем ${currentCard.login}?\n` +
@@ -271,13 +379,60 @@ export function render({ navigate, route }) {
}
}
+ async function onOpinionApply({ mode, nextKind, activeKind }) {
+ if (isBusy || !currentCard || !currentFlags) return;
+ if (!sessionLogin) {
+ window.alert('Для изменения связей нужен активный вход.');
+ return;
+ }
+ if (!state.session.storagePwdInMemory) {
+ window.alert('Нет storagePwd в памяти сессии. Выполните вход заново.');
+ return;
+ }
+
+ const confirmed = window.confirm(`Изменить мнение о пользователе ${currentCard.login}?`);
+ if (!confirmed) return;
+
+ isBusy = true;
+ status.className = 'status-line';
+ status.textContent = 'Сохранение отношения в блокчейн...';
+
+ try {
+ if (activeKind) {
+ await authService.setUserRelation({
+ login: sessionLogin,
+ toLogin: currentCard.login,
+ kind: activeKind,
+ enabled: false,
+ storagePwd: state.session.storagePwdInMemory,
+ });
+ }
+
+ if (mode === 'set') {
+ await authService.setUserRelation({
+ login: sessionLogin,
+ toLogin: currentCard.login,
+ kind: nextKind,
+ enabled: true,
+ storagePwd: state.session.storagePwdInMemory,
+ });
+ }
+ await refresh();
+ } catch (error) {
+ status.className = 'status-line is-unavailable';
+ status.textContent = `Ошибка изменения связи: ${error.message || 'unknown'}`;
+ window.alert(`Не удалось изменить связь: ${error.message || 'unknown'}`);
+ isBusy = false;
+ }
+ }
+
body.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const actionBtn = target.closest('[data-relation-action]');
const kind = String(actionBtn?.getAttribute('data-relation-action') || '');
if (!kind) return;
- onRelationAction(kind);
+ void onRelationAction(kind);
});
refresh();
diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js
index ebbe710..e5544cc 100644
--- a/shine-UI/js/services/auth-service.js
+++ b/shine-UI/js/services/auth-service.js
@@ -61,6 +61,9 @@ const CONNECTION_SUBTYPES = Object.freeze({
parent: { on: 50, off: 51 },
child: { on: 52, off: 53 },
sibling: { on: 54, off: 55 },
+ known_person: { on: 60, off: 61 },
+ shine_confirmed: { on: 70, off: 71 },
+ shine_seen: { on: 74, off: 75 },
});
function normalizeServerUrl(url) {
diff --git a/shine-UI/js/services/user-connections.js b/shine-UI/js/services/user-connections.js
index e4d3a79..658b76b 100644
--- a/shine-UI/js/services/user-connections.js
+++ b/shine-UI/js/services/user-connections.js
@@ -75,6 +75,12 @@ async function buildRelationsModel(login) {
inChildren: [],
outSiblings: [],
inSiblings: [],
+ outKnownPersons: [],
+ inKnownPersons: [],
+ outShineConfirmed: [],
+ inShineConfirmed: [],
+ outShineSeen: [],
+ inShineSeen: [],
};
}
@@ -117,6 +123,12 @@ async function buildRelationsModel(login) {
inChildren: readArray(graph, 'inChildren') || [],
outSiblings: readArray(graph, 'outSiblings') || [],
inSiblings: readArray(graph, 'inSiblings') || [],
+ outKnownPersons: readArray(graph, 'outKnownPersons') || [],
+ inKnownPersons: readArray(graph, 'inKnownPersons') || [],
+ outShineConfirmed: readArray(graph, 'outShineConfirmed') || [],
+ inShineConfirmed: readArray(graph, 'inShineConfirmed') || [],
+ outShineSeen: readArray(graph, 'outShineSeen') || [],
+ inShineSeen: readArray(graph, 'inShineSeen') || [],
};
}
@@ -151,6 +163,12 @@ export async function loadCurrentRelations() {
inChildren: [],
outSiblings: [],
inSiblings: [],
+ outKnownPersons: [],
+ inKnownPersons: [],
+ outShineConfirmed: [],
+ inShineConfirmed: [],
+ outShineSeen: [],
+ inShineSeen: [],
};
}
return buildRelationsModel(login);
@@ -170,6 +188,12 @@ export function relationFlagsForTarget(relations, targetLogin) {
inChild: listContainsLogin(relations?.inChildren, targetLogin),
outSibling: listContainsLogin(relations?.outSiblings, targetLogin),
inSibling: listContainsLogin(relations?.inSiblings, targetLogin),
+ outKnownPerson: listContainsLogin(relations?.outKnownPersons, targetLogin),
+ inKnownPerson: listContainsLogin(relations?.inKnownPersons, targetLogin),
+ outShineConfirmed: listContainsLogin(relations?.outShineConfirmed, targetLogin),
+ inShineConfirmed: listContainsLogin(relations?.inShineConfirmed, targetLogin),
+ outShineSeen: listContainsLogin(relations?.outShineSeen, targetLogin),
+ inShineSeen: listContainsLogin(relations?.inShineSeen, targetLogin),
};
}
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index f3de5dd..4d5f957 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -644,6 +644,8 @@
.avatar-image {
position: relative;
overflow: hidden;
+ display: grid;
+ place-items: center;
}
.avatar-image > .avatar-fallback,
@@ -1639,6 +1641,49 @@ textarea.input {
color: transparent;
}
+.user-rel-opinions-wrap {
+ display: grid;
+ gap: 8px;
+ padding: 6px 8px;
+ border-radius: 10px;
+ border: 1px dashed rgba(131, 196, 255, 0.45);
+ background: rgba(9, 18, 31, 0.42);
+}
+
+.user-rel-opinions-wrap.is-empty .user-rel-opinions-list {
+ display: none;
+}
+
+.user-rel-opinions-list {
+ display: grid;
+ gap: 6px;
+}
+
+.user-rel-opinion-item {
+ color: #d7e6ff;
+ line-height: 1.35;
+ font-size: 13px;
+}
+
+.user-rel-opinions-hint {
+ color: rgba(173, 199, 236, 0.9);
+ font-size: 12px;
+}
+
+.user-opinion-modal-btn {
+ text-align: left;
+}
+
+.user-opinion-modal-btn.is-add {
+ border-color: rgba(97, 170, 255, 0.7);
+ color: #9fcbff;
+}
+
+.user-opinion-modal-btn.is-remove {
+ border-color: rgba(255, 120, 120, 0.72);
+ color: #ff9b9b;
+}
+
.tabs {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -2219,6 +2264,11 @@ textarea.input {
background: radial-gradient(circle at 30% 30%, #8a73ff, #4f4bda 58%, #3b2b89);
}
+.channel-message-avatar.avatar-image {
+ display: grid;
+ place-items: center;
+}
+
.channel-message-author {
display: grid;
gap: 4px;
diff --git a/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java b/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java
index 7f80491..7d1f309 100644
--- a/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java
+++ b/shine-server-blockchain/src/main/java/blockchain/MsgSubType.java
@@ -113,6 +113,21 @@ public final class MsgSubType {
/** Удалить связь "брат/сестра". */
public static final short CONNECTION_UNSIBLING = 55;
+ /** Просто знаю этого человека. */
+ public static final short CONNECTION_KNOWN_PERSON = 60;
+ /** Не знаю этого человека. */
+ public static final short CONNECTION_UNKNOWN_PERSON = 61;
+
+ /** Точно уверен, что сияющий. */
+ public static final short CONNECTION_SHINE_CONFIRMED = 70;
+ /** Не подтверждаю, что сияющий. */
+ public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
+
+ /** Мало знаком, но видел сияющим. */
+ public static final short CONNECTION_SHINE_SEEN = 74;
+ /** Не отмечаю, что видел сияющим. */
+ public static final short CONNECTION_SHINE_UNSEEN = 75;
+
/* ===================== USER_PARAM (msg_type=4) ===================== */
/** Параметр профиля key/value (обе строки). */
diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java
index 5b037d8..bb7ef40 100644
--- a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java
+++ b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java
@@ -16,9 +16,13 @@ import java.util.Objects;
* FRIEND=10, UNFRIEND=11
* CONTACT=20, UNCONTACT=21
* FOLLOW=30, UNFOLLOW=31
+ * SPOUSE=40, UNSPOUSE=41
* PARENT=50, UNPARENT=51
* CHILD=52, UNCHILD=53
* SIBLING=54, UNSIBLING=55
+ * KNOWN_PERSON=60, UNKNOWN_PERSON=61
+ * SHINE_CONFIRMED=70, SHINE_UNCONFIRMED=71
+ * SHINE_SEEN=74, SHINE_UNSEEN=75
*
* bodyBytes (BigEndian), новый формат (toLogin НЕ ХРАНИМ):
* [4] lineCode
@@ -192,7 +196,13 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasL
|| v == (MsgSubType.CONNECTION_CHILD & 0xFFFF)
|| v == (MsgSubType.CONNECTION_UNCHILD & 0xFFFF)
|| v == (MsgSubType.CONNECTION_SIBLING & 0xFFFF)
- || v == (MsgSubType.CONNECTION_UNSIBLING & 0xFFFF);
+ || v == (MsgSubType.CONNECTION_UNSIBLING & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_KNOWN_PERSON & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_UNKNOWN_PERSON & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_SHINE_CONFIRMED & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_SHINE_UNCONFIRMED & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_SHINE_SEEN & 0xFFFF)
+ || v == (MsgSubType.CONNECTION_SHINE_UNSEEN & 0xFFFF);
}
@Override
diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
index 3145c0e..2afb541 100644
--- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
+++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java
@@ -66,6 +66,15 @@ public final class DatabaseInitializer {
public static final short CONNECTION_SIBLING = 54;
public static final short CONNECTION_UNSIBLING = 55;
+ public static final short CONNECTION_KNOWN_PERSON = 60;
+ public static final short CONNECTION_UNKNOWN_PERSON = 61;
+
+ public static final short CONNECTION_SHINE_CONFIRMED = 70;
+ public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
+
+ public static final short CONNECTION_SHINE_SEEN = 74;
+ public static final short CONNECTION_SHINE_UNSEEN = 75;
+
public static void createNewDB(String[] args) {
AppConfig config = AppConfig.getInstance();
String dbPath = config.getParam("db.path");
diff --git a/shine-server-db/src/main/java/shine/db/DatabaseTriggersInstaller.java b/shine-server-db/src/main/java/shine/db/DatabaseTriggersInstaller.java
index f4ff8a4..1327905 100644
--- a/shine-server-db/src/main/java/shine/db/DatabaseTriggersInstaller.java
+++ b/shine-server-db/src/main/java/shine/db/DatabaseTriggersInstaller.java
@@ -198,6 +198,9 @@ public final class DatabaseTriggersInstaller {
int PARENT = (int) DatabaseInitializer.CONNECTION_PARENT;
int CHILD = (int) DatabaseInitializer.CONNECTION_CHILD;
int SIBLING = (int) DatabaseInitializer.CONNECTION_SIBLING;
+ int KNOWN = (int) DatabaseInitializer.CONNECTION_KNOWN_PERSON;
+ int SHINE_CONF = (int) DatabaseInitializer.CONNECTION_SHINE_CONFIRMED;
+ int SHINE_SEEN = (int) DatabaseInitializer.CONNECTION_SHINE_SEEN;
int UNFRIEND = (int) DatabaseInitializer.CONNECTION_UNFRIEND;
int UNCONTACT = (int) DatabaseInitializer.CONNECTION_UNCONTACT;
@@ -206,13 +209,16 @@ public final class DatabaseTriggersInstaller {
int UNPARENT = (int) DatabaseInitializer.CONNECTION_UNPARENT;
int UNCHILD = (int) DatabaseInitializer.CONNECTION_UNCHILD;
int UNSIBLING = (int) DatabaseInitializer.CONNECTION_UNSIBLING;
+ int UNKNOWN = (int) DatabaseInitializer.CONNECTION_UNKNOWN_PERSON;
+ int SHINE_UNCONF = (int) DatabaseInitializer.CONNECTION_SHINE_UNCONFIRMED;
+ int SHINE_UNSEEN = (int) DatabaseInitializer.CONNECTION_SHINE_UNSEEN;
st.executeUpdate("""
CREATE TRIGGER IF NOT EXISTS trg_blocks_connection_state_ai
AFTER INSERT ON blocks
WHEN NEW.msg_type = 3
BEGIN
- -- FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING:
+ -- FRIEND/CONTACT/FOLLOW/SPOUSE/PARENT/CHILD/SIBLING/KNOWN_PERSON/SHINE_*:
-- 1) если записи нет — создаём
INSERT OR IGNORE INTO connections_state (
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
@@ -233,7 +239,7 @@ public final class DatabaseTriggersInstaller {
NEW.to_bch_name,
NEW.to_block_number,
NEW.to_block_hash
- WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d)
+ WHERE NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
AND COALESCE(
NEW.to_login,
CASE
@@ -264,7 +270,7 @@ public final class DatabaseTriggersInstaller {
ELSE NULL
END
)
- AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d)
+ AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d)
AND COALESCE(
NEW.to_login,
CASE
@@ -277,7 +283,7 @@ public final class DatabaseTriggersInstaller {
) IS NOT NULL
AND NEW.to_bch_name IS NOT NULL;
- -- UNFRIEND/UNCONTACT/UNFOLLOW/UNSPOUSE/UNPARENT/UNCHILD/UNSIBLING:
+ -- UNFRIEND/UNCONTACT/UNFOLLOW/UNSPOUSE/UNPARENT/UNCHILD/UNSIBLING/UNKNOWN_PERSON/SHINE_UN*:
-- удаляем соответствующее "позитивное" состояние
DELETE FROM connections_state
WHERE login = NEW.login
@@ -299,6 +305,9 @@ 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
ELSE rel_type
END
AND COALESCE(
@@ -311,11 +320,11 @@ public final class DatabaseTriggersInstaller {
ELSE NULL
END
) IS NOT NULL
- AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d);
+ AND NEW.msg_sub_type IN (%d, %d, %d, %d, %d, %d, %d, %d, %d, %d);
END;
""".formatted(
- FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING,
- FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING,
+ FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
+ FRIEND, CONTACT, FOLLOW, SPOUSE, PARENT, CHILD, SIBLING, KNOWN, SHINE_CONF, SHINE_SEEN,
UNFRIEND, FRIEND,
UNCONTACT, CONTACT,
@@ -324,8 +333,11 @@ public final class DatabaseTriggersInstaller {
UNPARENT, PARENT,
UNCHILD, CHILD,
UNSIBLING, SIBLING,
+ UNKNOWN, KNOWN,
+ SHINE_UNCONF, SHINE_CONF,
+ SHINE_UNSEEN, SHINE_SEEN,
- UNFRIEND, UNCONTACT, UNFOLLOW, UNSPOUSE, UNPARENT, UNCHILD, UNSIBLING
+ UNFRIEND, UNCONTACT, UNFOLLOW, UNSPOUSE, UNPARENT, UNCHILD, UNSIBLING, UNKNOWN, SHINE_UNCONF, SHINE_UNSEEN
));
}
diff --git a/shine-server-db/src/main/java/shine/db/MsgSubType.java b/shine-server-db/src/main/java/shine/db/MsgSubType.java
index 62f9093..7d3083a 100644
--- a/shine-server-db/src/main/java/shine/db/MsgSubType.java
+++ b/shine-server-db/src/main/java/shine/db/MsgSubType.java
@@ -40,8 +40,10 @@ public final class MsgSubType {
/* ===================== CONNECTION (msg_type=3) ===================== */
/**
* Совпадает с ConnectionBody:
- * 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, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55
+ * SET: CLOSE_FRIEND(=FRIEND)=10, CONTACT=20, FOLLOW=30, SPOUSE=40, PARENT=50, CHILD=52, SIBLING=54,
+ * KNOWN_PERSON=60, SHINE_CONFIRMED=70, SHINE_SEEN=74
+ * UNSET: UNCLOSE_FRIEND(=UNFRIEND)=11, UNCONTACT=21, UNFOLLOW=31, UNSPOUSE=41, UNPARENT=51, UNCHILD=53, UNSIBLING=55,
+ * UNKNOWN_PERSON=61, SHINE_UNCONFIRMED=71, SHINE_UNSEEN=75
*/
/** Добавить в близкие друзья (close friend). */
@@ -92,6 +94,24 @@ public final class MsgSubType {
/** Удалить связь "брат/сестра". */
public static final short CONNECTION_UNSIBLING = 55;
+ /** Просто знаю этого человека. */
+ public static final short CONNECTION_KNOWN_PERSON = 60;
+
+ /** Не знаю этого человека. */
+ public static final short CONNECTION_UNKNOWN_PERSON = 61;
+
+ /** Точно уверен, что сияющий. */
+ public static final short CONNECTION_SHINE_CONFIRMED = 70;
+
+ /** Не подтверждаю, что сияющий. */
+ public static final short CONNECTION_SHINE_UNCONFIRMED = 71;
+
+ /** Мало знаком, но видел сияющим. */
+ public static final short CONNECTION_SHINE_SEEN = 74;
+
+ /** Не отмечаю, что видел сияющим. */
+ public static final short CONNECTION_SHINE_UNSEEN = 75;
+
/* ===================== USER_PARAM (msg_type=4) ===================== */
/** Параметр профиля key/value (обе строки). */
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java
index 5406117..397895e 100644
--- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/Net_GetUserConnectionsGraph_Handler.java
@@ -55,11 +55,18 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
List inChildren = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_CHILD);
List outSiblings = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
List inSiblings = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SIBLING);
+ List outKnownPersons = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON);
+ List inKnownPersons = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_KNOWN_PERSON);
+ List outShineConfirmed = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED);
+ List inShineConfirmed = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_CONFIRMED);
+ List outShineSeen = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
+ List inShineSeen = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, canonicalLogin, MsgSubType.CONNECTION_SHINE_SEEN);
LinkedHashSet allLogins = new LinkedHashSet<>();
allLogins.add(canonicalLogin);
addAllLogins(allLogins, outFriends, inFriends, outContacts, inContacts, outFollows, inFollows,
- outSpouses, inSpouses, outParents, inParents, outChildren, inChildren, outSiblings, inSiblings);
+ outSpouses, inSpouses, outParents, inParents, outChildren, inChildren, outSiblings, inSiblings,
+ outKnownPersons, inKnownPersons, outShineConfirmed, inShineConfirmed, outShineSeen, inShineSeen);
Map metaByLogin = loadUserMeta(c, allLogins);
List spouseLogins = mergeUnique(outSpouses, inSpouses);
@@ -86,6 +93,12 @@ public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
resp.setInChildren(inChildren);
resp.setOutSiblings(outSiblings);
resp.setInSiblings(inSiblings);
+ resp.setOutKnownPersons(outKnownPersons);
+ resp.setInKnownPersons(inKnownPersons);
+ resp.setOutShineConfirmed(outShineConfirmed);
+ resp.setInShineConfirmed(inShineConfirmed);
+ resp.setOutShineSeen(outShineSeen);
+ resp.setInShineSeen(inShineSeen);
resp.setParents(toRelativeItems(parentLogins, metaByLogin));
resp.setChildren(toRelativeItems(childLogins, metaByLogin));
resp.setSiblings(toRelativeItems(siblingLogins, metaByLogin));
diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java
index 33b0be4..feced17 100644
--- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java
+++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/connections/entyties/Net_GetUserConnectionsGraph_Response.java
@@ -21,6 +21,12 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
private List inChildren = new ArrayList<>();
private List outSiblings = new ArrayList<>();
private List inSiblings = new ArrayList<>();
+ private List outKnownPersons = new ArrayList<>();
+ private List inKnownPersons = new ArrayList<>();
+ private List outShineConfirmed = new ArrayList<>();
+ private List inShineConfirmed = new ArrayList<>();
+ private List outShineSeen = new ArrayList<>();
+ private List inShineSeen = new ArrayList<>();
private List parents = new ArrayList<>();
private List children = new ArrayList<>();
private List siblings = new ArrayList<>();
@@ -102,6 +108,18 @@ public class Net_GetUserConnectionsGraph_Response extends Net_Response {
public void setOutSiblings(List outSiblings) { this.outSiblings = outSiblings; }
public List getInSiblings() { return inSiblings; }
public void setInSiblings(List inSiblings) { this.inSiblings = inSiblings; }
+ public List getOutKnownPersons() { return outKnownPersons; }
+ public void setOutKnownPersons(List outKnownPersons) { this.outKnownPersons = outKnownPersons; }
+ public List getInKnownPersons() { return inKnownPersons; }
+ public void setInKnownPersons(List inKnownPersons) { this.inKnownPersons = inKnownPersons; }
+ public List getOutShineConfirmed() { return outShineConfirmed; }
+ public void setOutShineConfirmed(List outShineConfirmed) { this.outShineConfirmed = outShineConfirmed; }
+ public List getInShineConfirmed() { return inShineConfirmed; }
+ public void setInShineConfirmed(List inShineConfirmed) { this.inShineConfirmed = inShineConfirmed; }
+ public List getOutShineSeen() { return outShineSeen; }
+ public void setOutShineSeen(List outShineSeen) { this.outShineSeen = outShineSeen; }
+ public List getInShineSeen() { return inShineSeen; }
+ public void setInShineSeen(List inShineSeen) { this.inShineSeen = inShineSeen; }
public List getParents() { return parents; }
public void setParents(List parents) { this.parents = parents; }
public List getChildren() { return children; }