SHA256
Добавить opinion-связи и обновить UI связей в профиле
This commit is contained in:
@@ -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 `
|
||||
<div class="card stack user-relations-list">
|
||||
@@ -122,10 +164,65 @@ function renderRelations(flags) {
|
||||
<button class="ghost-btn user-rel-action" type="button" data-relation-action="${row.kind}">${escapeHtml(row.button)}</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
<div class="user-rel-opinions-wrap ${hasOpinion ? '' : 'is-empty'}">
|
||||
<div class="user-rel-opinions-list">
|
||||
${opinionItems.map((item) => `
|
||||
<div class="user-rel-opinion-item">${escapeHtml(item.text)}</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="user-rel-opinions-hint">Добавьте одну из этих трёх формулировок.</div>
|
||||
</div>
|
||||
<div class="user-rel-row">
|
||||
<span class="user-rel-text">${hasOpinion ? 'Мнение уже добавлено.' : 'Пока нет дополнительной связи.'}</span>
|
||||
<button class="ghost-btn user-rel-action user-rel-opinion-btn" type="button" data-relation-action="opinion-menu">${hasOpinion ? 'Изменить связи' : 'Добавить связь'}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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) => `<button class="secondary-btn user-opinion-modal-btn is-add" type="button" data-opinion-kind="${item.kind}" data-opinion-mode="set">Добавить: ${item.title}</button>`)
|
||||
.join('');
|
||||
const removeHtml = activeKind
|
||||
? `<button class="secondary-btn user-opinion-modal-btn is-remove" type="button" data-opinion-kind="${activeKind}" data-opinion-mode="remove">Убрать мнение</button>`
|
||||
: '';
|
||||
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="user-opinion-modal">
|
||||
<div class="modal-card stack">
|
||||
<h3 class="modal-title">${activeKind ? 'Изменить связи' : 'Добавить связь'}</h3>
|
||||
<div class="stack">${rowsHtml}${removeHtml}</div>
|
||||
<button class="secondary-btn" type="button" id="user-opinion-modal-close">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user