Refine direct message profile data

This commit is contained in:
AidarKC
2026-09-02 14:56:33 +04:00
parent 065616a18b
commit 1373283947
5 changed files with 77 additions and 29 deletions
@@ -7,6 +7,8 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import shine.db.MsgSubType;
/** Fast denormalized projection for user cards shown in dialogs/relations/profile lists. */
public final class UserProfileStateDAO {
private static volatile UserProfileStateDAO instance;
@@ -32,25 +34,26 @@ public final class UserProfileStateDAO {
}
/** Effective outgoing social relation with priority close_friend > friend > contact > none. */
/**
* Effective outgoing social relation with priority close_friend > friend > contact > none.
*
* Use the canonical relation lookup instead of comparing only connections_state.to_login:
* older/current blocks may address a user by blockchain name, so a direct to_login-only
* query can incorrectly report "none" even when ListContacts/Connections sees the relation.
*/
public String getEffectiveRelationType(Connection c, String ownerLogin, String targetLogin) throws SQLException {
if (ownerLogin == null || ownerLogin.isBlank() || targetLogin == null || targetLogin.isBlank()) return "none";
try (PreparedStatement ps = c.prepareStatement("""
SELECT CASE
WHEN EXISTS(SELECT 1 FROM connections_state WHERE LOWER(login)=LOWER(?) AND LOWER(to_login)=LOWER(?) AND rel_type=10) THEN 'close_friend'
WHEN EXISTS(SELECT 1 FROM connections_state WHERE LOWER(login)=LOWER(?) AND LOWER(to_login)=LOWER(?) AND rel_type=14) THEN 'friend'
WHEN EXISTS(SELECT 1 FROM connections_state WHERE LOWER(login)=LOWER(?) AND LOWER(to_login)=LOWER(?) AND rel_type=20) THEN 'contact'
ELSE 'none' END AS relation_type
""")) {
int i = 1;
for (int n = 0; n < 3; n++) {
ps.setString(i++, ownerLogin);
ps.setString(i++, targetLogin);
}
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("relation_type") : "none";
}
ConnectionsStateDAO relations = ConnectionsStateDAO.getInstance();
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
return "close_friend";
}
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_FRIEND)) {
return "friend";
}
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_CONTACT)) {
return "contact";
}
return "none";
}
public List<RelationCard> listRelations(Connection c, String ownerLogin, String listType, int limit, int offset) throws SQLException {
@@ -130,12 +130,17 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
private static Net_GetDirectMessages_Response.Avatar parseAvatar(String value) {
Net_GetDirectMessages_Response.Avatar out = new Net_GetDirectMessages_Response.Avatar();
String raw = value == null ? "" : value.trim();
java.util.regex.Matcher ar = java.util.regex.Pattern.compile("(?:^|,)\\s*AR:([A-Za-z0-9_-]{43})(?:,|$)").matcher(raw);
if (!ar.find()) ar = java.util.regex.Pattern.compile("AR:([A-Za-z0-9_-]{43})").matcher(raw);
if (ar.find()) out.setAr(ar.group(1));
java.util.regex.Matcher sha = java.util.regex.Pattern.compile("(?:^|,)\\s*SHA256:([A-Fa-f0-9]{64})(?:,|$)").matcher(raw);
if (!sha.find()) sha = java.util.regex.Pattern.compile("SHA256:([A-Fa-f0-9]{64})").matcher(raw);
if (sha.find()) out.setSha256Hex(sha.group(1).toLowerCase());
// Do not call Matcher.find() twice on the same matcher: the first successful
// call advances it and the second one can make a perfectly valid avatar vanish.
java.util.regex.Matcher ar = java.util.regex.Pattern.compile("AR:([A-Za-z0-9_-]{43})").matcher(raw);
if (ar.find()) {
out.setAr(ar.group(1));
}
java.util.regex.Matcher sha = java.util.regex.Pattern.compile("SHA256:([A-Fa-f0-9]{64})").matcher(raw);
if (sha.find()) {
out.setSha256Hex(sha.group(1).toLowerCase());
}
return out;
}
+15 -2
View File
@@ -52,6 +52,18 @@ function cleanChannelMessagePreview(text) {
|| (parsed.attachments.length ? 'Вложение' : 'Ждем ваших начинаний');
}
// Keep the channel-list preview tolerant to small API naming changes. The current
// server uses lastMessage.text/createdAtMs; legacy/alternate payloads are accepted
// so the third line (last message + time) does not silently disappear.
function resolveChannelLastMessage(summary) {
const row = summary?.lastMessage || summary?.latestMessage || summary?.last_message || null;
if (!row || typeof row !== 'object') return { text: '', createdAtMs: 0 };
return {
text: String(row.text ?? row.messageText ?? row.preview ?? row.body ?? '').trim(),
createdAtMs: Number(row.createdAtMs ?? row.timeMs ?? row.created_at_ms ?? 0),
};
}
function isChannelsDemoMode() {
try {
const qs = new URLSearchParams(window.location.search);
@@ -719,6 +731,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
const isOwn = bucketKey === 'own';
const title = displayTitle || channelName;
const technicalLabel = `${ownerLogin} / ${channelName}`;
const lastMessage = resolveChannelLastMessage(summary);
return {
id: rowId,
@@ -737,10 +750,10 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
channelDescription,
channelTypeCode,
channelTypeVersion,
messagePreview: cleanChannelMessagePreview(summary?.lastMessage?.text),
messagePreview: cleanChannelMessagePreview(lastMessage.text),
messagesCount: Number(summary?.messagesCount || 0),
unreadCount: Number(summary?.unreadCount || 0),
lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0),
lastMessageAt: Number(lastMessage.createdAtMs || 0),
isOwnChannel: isOwn,
isSubscribed: !isOwn,
notificationsEnabled: notificationsState[rowId] === true,
@@ -116,9 +116,6 @@ export async function loadProfileSnapshot(login) {
});
}
const firstName = String(fields.find((field) => field.key === 'first_name')?.value || '').trim();
const lastName = String(fields.find((field) => field.key === 'last_name')?.value || '').trim();
const latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
@@ -160,8 +157,6 @@ export async function loadProfileSnapshot(login) {
return {
fields,
firstName,
lastName,
toggles,
accountRole,
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
+32
View File
@@ -10759,3 +10759,35 @@ body.chat-topbar-overlay .page-header.app-topbar-shell .header-center {
font-weight: 500;
color: rgba(189,207,232,0.72);
}
/* ===== DM header regression fix (2026-09-02) =====
* The peer identity button intentionally spans the whole free header area.
* Its avatar uses a 119% glass overlay, so neither the center cell nor the
* button may clip overflow. Text keeps its own ellipsis boundary instead.
*/
body.chat-topbar-overlay .page-header.app-topbar-shell .header-center {
overflow: visible !important;
}
.chat-header-peer-btn {
overflow: visible !important;
padding-inline: 2px !important;
}
.chat-header-peer-btn .chat-header-avatar-slot {
width: 44px !important;
height: 44px !important;
min-width: 44px !important;
min-height: 44px !important;
flex: 0 0 44px !important;
overflow: visible !important;
}
.chat-header-peer-btn .chat-header-avatar-slot .chat-header-avatar {
overflow: visible !important;
}
.chat-header-peer-text {
flex: 1 1 auto;
overflow: hidden;
}