SHA256
Refine direct message profile data
This commit is contained in:
@@ -7,6 +7,8 @@ import java.sql.SQLException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import shine.db.MsgSubType;
|
||||||
|
|
||||||
/** Fast denormalized projection for user cards shown in dialogs/relations/profile lists. */
|
/** Fast denormalized projection for user cards shown in dialogs/relations/profile lists. */
|
||||||
public final class UserProfileStateDAO {
|
public final class UserProfileStateDAO {
|
||||||
private static volatile UserProfileStateDAO instance;
|
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 {
|
public String getEffectiveRelationType(Connection c, String ownerLogin, String targetLogin) throws SQLException {
|
||||||
if (ownerLogin == null || ownerLogin.isBlank() || targetLogin == null || targetLogin.isBlank()) return "none";
|
if (ownerLogin == null || ownerLogin.isBlank() || targetLogin == null || targetLogin.isBlank()) return "none";
|
||||||
try (PreparedStatement ps = c.prepareStatement("""
|
ConnectionsStateDAO relations = ConnectionsStateDAO.getInstance();
|
||||||
SELECT CASE
|
if (relations.hasOutgoingByRelTypeCanonical(c, ownerLogin, targetLogin, MsgSubType.CONNECTION_CLOSE_FRIEND)) {
|
||||||
WHEN EXISTS(SELECT 1 FROM connections_state WHERE LOWER(login)=LOWER(?) AND LOWER(to_login)=LOWER(?) AND rel_type=10) THEN 'close_friend'
|
return "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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 {
|
public List<RelationCard> listRelations(Connection c, String ownerLogin, String listType, int limit, int offset) throws SQLException {
|
||||||
|
|||||||
+11
-6
@@ -130,12 +130,17 @@ public class Net_GetDirectMessages_Handler implements JsonMessageHandler {
|
|||||||
private static Net_GetDirectMessages_Response.Avatar parseAvatar(String value) {
|
private static Net_GetDirectMessages_Response.Avatar parseAvatar(String value) {
|
||||||
Net_GetDirectMessages_Response.Avatar out = new Net_GetDirectMessages_Response.Avatar();
|
Net_GetDirectMessages_Response.Avatar out = new Net_GetDirectMessages_Response.Avatar();
|
||||||
String raw = value == null ? "" : value.trim();
|
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);
|
// Do not call Matcher.find() twice on the same matcher: the first successful
|
||||||
if (ar.find()) out.setAr(ar.group(1));
|
// call advances it and the second one can make a perfectly valid avatar vanish.
|
||||||
java.util.regex.Matcher sha = java.util.regex.Pattern.compile("(?:^|,)\\s*SHA256:([A-Fa-f0-9]{64})(?:,|$)").matcher(raw);
|
java.util.regex.Matcher ar = java.util.regex.Pattern.compile("AR:([A-Za-z0-9_-]{43})").matcher(raw);
|
||||||
if (!sha.find()) sha = java.util.regex.Pattern.compile("SHA256:([A-Fa-f0-9]{64})").matcher(raw);
|
if (ar.find()) {
|
||||||
if (sha.find()) out.setSha256Hex(sha.group(1).toLowerCase());
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,18 @@ function cleanChannelMessagePreview(text) {
|
|||||||
|| (parsed.attachments.length ? 'Вложение' : 'Ждем ваших начинаний');
|
|| (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() {
|
function isChannelsDemoMode() {
|
||||||
try {
|
try {
|
||||||
const qs = new URLSearchParams(window.location.search);
|
const qs = new URLSearchParams(window.location.search);
|
||||||
@@ -719,6 +731,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
|
|||||||
const isOwn = bucketKey === 'own';
|
const isOwn = bucketKey === 'own';
|
||||||
const title = displayTitle || channelName;
|
const title = displayTitle || channelName;
|
||||||
const technicalLabel = `${ownerLogin} / ${channelName}`;
|
const technicalLabel = `${ownerLogin} / ${channelName}`;
|
||||||
|
const lastMessage = resolveChannelLastMessage(summary);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: rowId,
|
id: rowId,
|
||||||
@@ -737,10 +750,10 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
|
|||||||
channelDescription,
|
channelDescription,
|
||||||
channelTypeCode,
|
channelTypeCode,
|
||||||
channelTypeVersion,
|
channelTypeVersion,
|
||||||
messagePreview: cleanChannelMessagePreview(summary?.lastMessage?.text),
|
messagePreview: cleanChannelMessagePreview(lastMessage.text),
|
||||||
messagesCount: Number(summary?.messagesCount || 0),
|
messagesCount: Number(summary?.messagesCount || 0),
|
||||||
unreadCount: Number(summary?.unreadCount || 0),
|
unreadCount: Number(summary?.unreadCount || 0),
|
||||||
lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0),
|
lastMessageAt: Number(lastMessage.createdAtMs || 0),
|
||||||
isOwnChannel: isOwn,
|
isOwnChannel: isOwn,
|
||||||
isSubscribed: !isOwn,
|
isSubscribed: !isOwn,
|
||||||
notificationsEnabled: notificationsState[rowId] === true,
|
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 latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
|
||||||
const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
|
const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
|
||||||
const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
|
const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
|
||||||
@@ -160,8 +157,6 @@ export async function loadProfileSnapshot(login) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
fields,
|
fields,
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
toggles,
|
toggles,
|
||||||
accountRole,
|
accountRole,
|
||||||
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
|
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
|
||||||
|
|||||||
@@ -10759,3 +10759,35 @@ body.chat-topbar-overlay .page-header.app-topbar-shell .header-center {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: rgba(189,207,232,0.72);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user