feat: improve realtime messaging and media handling
This commit is contained in:
+677
-9
@@ -132,7 +132,7 @@ function fetchMessageRows(PDO $db, array $messageIds): array
|
||||
|
||||
$messages = [];
|
||||
foreach ($idsByTable as $table => $ids) {
|
||||
$sql = 'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_searchable_key`, `_dt`, `_content_type`, `_to`'
|
||||
$sql = 'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_data`, `_searchable_key`, `_dt`, `_content_type`, `_to`'
|
||||
. ' from `' . $table . '` where `_mid` in (' . placeholders($ids) . ')';
|
||||
$statement = $db->prepare($sql);
|
||||
$statement->execute($ids);
|
||||
@@ -232,12 +232,21 @@ function messagePreviewText(?array $message): string
|
||||
return '';
|
||||
}
|
||||
$attachment = is_array($message['attachment'] ?? null) ? $message['attachment'] : null;
|
||||
$contentType = (int)($message['contentType'] ?? 0);
|
||||
$messageType = (string)($message['type'] ?? '');
|
||||
$placeholderIsImage = $contentType === 3 || $messageType === 'image';
|
||||
$text = trim((string)($message['text'] ?? ''));
|
||||
if ($contentType !== 1 && $text === '[暂不支持的消息类型]') {
|
||||
$text = '';
|
||||
}
|
||||
if ($placeholderIsImage && ($text === '[图片]' || $text === '图片')) {
|
||||
$text = '';
|
||||
}
|
||||
if ($attachment) {
|
||||
$text = trim((string)($message['text'] ?? ''));
|
||||
return $text !== '' ? $text : attachmentFallbackText($attachment);
|
||||
}
|
||||
|
||||
return (string)($message['text'] ?? '');
|
||||
return $text;
|
||||
}
|
||||
|
||||
function fetchUserInfoMap(PDO $db, array $userIds, string $ownerId = ''): array
|
||||
@@ -405,6 +414,42 @@ function isWebGroupTarget(PDO $appDb, string $targetId): bool
|
||||
return (bool)$statement->fetchColumn();
|
||||
}
|
||||
|
||||
function nativeGroupRoleFromType(mixed $type): string
|
||||
{
|
||||
$value = (int)$type;
|
||||
if ($value === 2) {
|
||||
return 'owner';
|
||||
}
|
||||
if ($value === 1) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
return 'member';
|
||||
}
|
||||
|
||||
function isNativeGroupTarget(PDO $db, string $targetId): bool
|
||||
{
|
||||
if ($targetId === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$statement = $db->prepare('select 1 from `t_group` where `_gid` = ? and `_deleted` = 0 limit 1');
|
||||
$statement->execute([$targetId]);
|
||||
|
||||
return (bool)$statement->fetchColumn();
|
||||
}
|
||||
|
||||
function nativeGroupRole(PDO $db, string $userId, string $groupId): string
|
||||
{
|
||||
$statement = $db->prepare(
|
||||
'select `_type` from `t_group_member` where `_gid` = ? and `_mid` = ? limit 1'
|
||||
);
|
||||
$statement->execute([$groupId, $userId]);
|
||||
$type = $statement->fetchColumn();
|
||||
|
||||
return $type === false ? '' : nativeGroupRoleFromType($type);
|
||||
}
|
||||
|
||||
function markWebGroupRead(PDO $appDb, string $userId, string $groupId, int $timestamp): void
|
||||
{
|
||||
if ($userId === '' || $groupId === '' || $timestamp <= 0) {
|
||||
@@ -422,6 +467,34 @@ function markWebGroupRead(PDO $appDb, string $userId, string $groupId, int $time
|
||||
$statement->execute([$groupId, $userId, $timestamp, $now]);
|
||||
}
|
||||
|
||||
function markNativeGroupRead(PDO $db, string $userId, string $groupId, int $timestamp): void
|
||||
{
|
||||
if ($userId === '' || $groupId === '' || $timestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$statement = $db->prepare(
|
||||
'select `id`, `_dt` from `t_read_report`'
|
||||
. ' where `_uid` = ? and `_type` = 1 and `_line` = 0 and `_target` = ?'
|
||||
. ' order by `_dt` desc limit 1'
|
||||
);
|
||||
$statement->execute([$userId, $groupId]);
|
||||
$row = $statement->fetch();
|
||||
|
||||
if ($row) {
|
||||
if ($timestamp > (int)$row['_dt']) {
|
||||
$update = $db->prepare('update `t_read_report` set `_dt` = ? where `id` = ?');
|
||||
$update->execute([$timestamp, (int)$row['id']]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$insert = $db->prepare(
|
||||
'insert into `t_read_report` (`_uid`, `_type`, `_line`, `_target`, `_dt`) values (?, 1, 0, ?, ?)'
|
||||
);
|
||||
$insert->execute([$userId, $groupId, $timestamp]);
|
||||
}
|
||||
|
||||
function webGroupRole(PDO $appDb, string $userId, string $groupId): string
|
||||
{
|
||||
$statement = $appDb->prepare(
|
||||
@@ -434,6 +507,50 @@ function webGroupRole(PDO $appDb, string $userId, string $groupId): string
|
||||
return is_string($role) && $role !== '' ? $role : '';
|
||||
}
|
||||
|
||||
function loadNativeGroupReadStateMap(PDO $db, string $userId, array $groupIds): array
|
||||
{
|
||||
$groupIds = array_values(array_unique(array_filter(array_map('strval', $groupIds))));
|
||||
if ($userId === '' || !$groupIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$statement = $db->prepare(
|
||||
'select `_target`, max(`_dt`) as `_read_at` from `t_read_report`'
|
||||
. ' where `_uid` = ? and `_type` = 1 and `_line` = 0'
|
||||
. ' and `_target` in (' . placeholders($groupIds) . ')'
|
||||
. ' group by `_target`'
|
||||
);
|
||||
$statement->execute(array_merge([$userId], $groupIds));
|
||||
|
||||
$readAtByGroup = [];
|
||||
foreach ($statement->fetchAll() as $row) {
|
||||
$readAtByGroup[(string)$row['_target']] = (int)$row['_read_at'];
|
||||
}
|
||||
|
||||
return $readAtByGroup;
|
||||
}
|
||||
|
||||
function fetchNativeGroupMemberMap(PDO $db, string $groupId, array $userIds): array
|
||||
{
|
||||
$userIds = array_values(array_unique(array_filter(array_map('strval', $userIds))));
|
||||
if ($groupId === '' || !$userIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$statement = $db->prepare(
|
||||
'select `_mid`, `_alias`, `_type`, `_dt`, `_create_dt` from `t_group_member`'
|
||||
. ' where `_gid` = ? and `_mid` in (' . placeholders($userIds) . ')'
|
||||
);
|
||||
$statement->execute(array_merge([$groupId], $userIds));
|
||||
|
||||
$members = [];
|
||||
foreach ($statement->fetchAll() as $row) {
|
||||
$members[(string)$row['_mid']] = $row;
|
||||
}
|
||||
|
||||
return $members;
|
||||
}
|
||||
|
||||
function formatWebGroupMessage(
|
||||
array $row,
|
||||
string $currentUserId,
|
||||
@@ -508,6 +625,314 @@ function formatWebGroupMessage(
|
||||
];
|
||||
}
|
||||
|
||||
function cleanupNativeMediaUrl(string $url): string
|
||||
{
|
||||
return rtrim($url, " \t\r\n\0\x0B\"'<>.,;)]}");
|
||||
}
|
||||
|
||||
function nativeMediaUrlScore(string $url): int
|
||||
{
|
||||
$path = strtolower((string)(parse_url($url, PHP_URL_PATH) ?: $url));
|
||||
$score = 0;
|
||||
if (preg_match('/\.(?:jpe?g|png|gif|webp|bmp|avif)$/i', $path)) {
|
||||
$score += 30;
|
||||
}
|
||||
if (strpos($path, 'image') !== false || strpos($path, 'media') !== false || strpos($path, 'upload') !== false) {
|
||||
$score += 5;
|
||||
}
|
||||
if (strpos($path, 'thumb') !== false || strpos($path, 'thumbnail') !== false) {
|
||||
$score -= 10;
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
function extractNativeMediaUrlFromData($data): string
|
||||
{
|
||||
if (is_resource($data)) {
|
||||
$data = stream_get_contents($data);
|
||||
}
|
||||
if (!is_string($data) || $data === '') {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match_all("~https?://[^\\s\"'<>\\x00]+~i", $data, $matches)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$urls = [];
|
||||
foreach ($matches[0] as $url) {
|
||||
$url = cleanupNativeMediaUrl((string)$url);
|
||||
if ($url !== '' && filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
$urls[$url] = $url;
|
||||
}
|
||||
}
|
||||
if (!$urls) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$urls = array_values($urls);
|
||||
usort($urls, static function (string $left, string $right): int {
|
||||
$scoreDelta = nativeMediaUrlScore($right) <=> nativeMediaUrlScore($left);
|
||||
return $scoreDelta !== 0 ? $scoreDelta : strlen($right) <=> strlen($left);
|
||||
});
|
||||
|
||||
return $urls[0] ?? '';
|
||||
}
|
||||
|
||||
function nativeAttachmentName(string $url, string $type): string
|
||||
{
|
||||
$path = (string)(parse_url($url, PHP_URL_PATH) ?: '');
|
||||
$name = trim(rawurldecode((string)basename($path)));
|
||||
if ($name !== '' && $name !== '.' && $name !== '/') {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return $type === 'image' ? '图片' : '文件';
|
||||
}
|
||||
|
||||
function nativeAttachmentMime(string $url, string $type): string
|
||||
{
|
||||
if ($type !== 'image') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$path = strtolower((string)(parse_url($url, PHP_URL_PATH) ?: ''));
|
||||
if (preg_match('/\.jpe?g$/', $path)) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if (preg_match('/\.png$/', $path)) {
|
||||
return 'image/png';
|
||||
}
|
||||
if (preg_match('/\.gif$/', $path)) {
|
||||
return 'image/gif';
|
||||
}
|
||||
if (preg_match('/\.webp$/', $path)) {
|
||||
return 'image/webp';
|
||||
}
|
||||
if (preg_match('/\.bmp$/', $path)) {
|
||||
return 'image/bmp';
|
||||
}
|
||||
if (preg_match('/\.avif$/', $path)) {
|
||||
return 'image/avif';
|
||||
}
|
||||
|
||||
return 'image/*';
|
||||
}
|
||||
|
||||
function nativeAttachmentFromRow(array $row, ?array $attachment): ?array
|
||||
{
|
||||
if ($attachment !== null) {
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
$contentType = (int)($row['_content_type'] ?? 0);
|
||||
if ($contentType !== 3 && $contentType !== 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = extractNativeMediaUrlFromData($row['_data'] ?? '');
|
||||
if ($url === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $contentType === 3 ? 'image' : 'file';
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'url' => $url,
|
||||
'name' => nativeAttachmentName($url, $type),
|
||||
'mime' => nativeAttachmentMime($url, $type),
|
||||
'size' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
function isNativePlaceholderText(string $text, int $contentType): bool
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return false;
|
||||
}
|
||||
if ($contentType !== 1 && $text === '[暂不支持的消息类型]') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $contentType === 3 && ($text === '[图片]' || $text === '图片');
|
||||
}
|
||||
|
||||
function nativeDisplayText(string $searchableText, int $contentType, bool $hasAttachment, bool $isRecalled = false): string
|
||||
{
|
||||
$text = trim($searchableText);
|
||||
if (isNativePlaceholderText($text, $contentType)) {
|
||||
return '';
|
||||
}
|
||||
if ($text === '' && !$isRecalled) {
|
||||
return nativeMessageFallbackText($contentType, $hasAttachment);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
function nativeMessageFallbackText(int $contentType, bool $hasAttachment): string
|
||||
{
|
||||
if ($hasAttachment) {
|
||||
return '';
|
||||
}
|
||||
if ($contentType === 5) {
|
||||
return '[文件]';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function nativeMessageTableForDate(DateTimeImmutable $date): string
|
||||
{
|
||||
$localDate = $date->setTimezone(new DateTimeZone(IM_DB_TIMEZONE));
|
||||
$year = (int)$localDate->format('Y');
|
||||
$month = (int)$localDate->format('n') - 1;
|
||||
|
||||
return 't_messages_' . (($year % 3) * 12 + $month);
|
||||
}
|
||||
|
||||
function recentNativeMessageTables(int $monthsBack = 3): array
|
||||
{
|
||||
$monthsBack = max(1, min($monthsBack, 12));
|
||||
$cursor = (new DateTimeImmutable('now', new DateTimeZone(IM_DB_TIMEZONE)))
|
||||
->modify('first day of this month');
|
||||
$tables = [];
|
||||
|
||||
for ($index = 0; $index < $monthsBack; $index++) {
|
||||
$tables[] = nativeMessageTableForDate($cursor);
|
||||
$cursor = $cursor->modify('-1 month');
|
||||
}
|
||||
|
||||
return array_values(array_unique($tables));
|
||||
}
|
||||
|
||||
function loadNativeGroupRowsFromGlobal(PDO $db, array $groupIds, int $limit): array
|
||||
{
|
||||
$groupIds = array_values(array_unique(array_filter(array_map('strval', $groupIds))));
|
||||
if (!$groupIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$limit = min(max($limit, 50), 1000);
|
||||
$rows = [];
|
||||
foreach (recentNativeMessageTables(3) as $table) {
|
||||
try {
|
||||
$statement = $db->prepare(
|
||||
'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_data`, `_searchable_key`, `_dt`, `_content_type`, `_to`'
|
||||
. ' from `' . $table . '`'
|
||||
. ' where `_type` = 1 and `_line` = 0'
|
||||
. ' and `_target` in (' . placeholders($groupIds) . ')'
|
||||
. ' order by `_mid` desc limit ' . $limit
|
||||
);
|
||||
$statement->execute($groupIds);
|
||||
} catch (Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($statement->fetchAll() as $row) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
usort($rows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid']));
|
||||
|
||||
$uniqueRows = [];
|
||||
$seenMessageIds = [];
|
||||
foreach ($rows as $row) {
|
||||
$messageId = (string)$row['_mid'];
|
||||
if ($messageId === '' || isset($seenMessageIds[$messageId])) {
|
||||
continue;
|
||||
}
|
||||
$seenMessageIds[$messageId] = true;
|
||||
$uniqueRows[] = $row;
|
||||
if (count($uniqueRows) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $uniqueRows;
|
||||
}
|
||||
|
||||
function formatNativeGroupMessage(
|
||||
array $row,
|
||||
string $currentUserId,
|
||||
array $group,
|
||||
?array $attachment = null,
|
||||
array $users = [],
|
||||
array $members = []
|
||||
): array {
|
||||
$groupId = (string)$row['_target'];
|
||||
$from = (string)$row['_from'];
|
||||
$senderUser = $users[$from] ?? [];
|
||||
$senderMember = $members[$from] ?? [];
|
||||
$senderAlias = trim((string)($senderMember['_alias'] ?? ''));
|
||||
$senderContactAlias = trim((string)($senderUser['_alias'] ?? ''));
|
||||
$senderFriendAlias = trim((string)($senderUser['_friend_alias'] ?? ''));
|
||||
$senderRemarkAlias = trim((string)($senderUser['_remark_alias'] ?? ''));
|
||||
$senderRawName = trim((string)($senderUser['_display_name'] ?? ''))
|
||||
?: trim((string)($senderUser['_name'] ?? ''))
|
||||
?: trim((string)($senderUser['_mobile'] ?? ''))
|
||||
?: $from;
|
||||
$senderName = $senderContactAlias
|
||||
?: $senderAlias
|
||||
?: $senderRawName;
|
||||
$senderRole = nativeGroupRoleFromType($senderMember['_type'] ?? 0);
|
||||
$messageId = (string)$row['_mid'];
|
||||
$timestamp = timestampMs((string)($row['_dt'] ?? ''), $messageId);
|
||||
$contentType = (int)($row['_content_type'] ?? 0);
|
||||
$isRecalled = $contentType === 80;
|
||||
$direction = $from === $currentUserId ? 'out' : 'in';
|
||||
$attachment = nativeAttachmentFromRow($row, $attachment);
|
||||
$messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown');
|
||||
$text = nativeDisplayText((string)($row['_searchable_key'] ?? ''), $contentType, $attachment !== null, $isRecalled);
|
||||
|
||||
return [
|
||||
'id' => $messageId,
|
||||
'messageUid' => $messageId,
|
||||
'conversationId' => 'group-' . $groupId,
|
||||
'from' => $from,
|
||||
'senderRole' => $senderRole,
|
||||
'senderName' => $senderName,
|
||||
'senderPortrait' => (string)($senderUser['_portrait'] ?? ''),
|
||||
'senderProfile' => [
|
||||
'uid' => $from,
|
||||
'displayName' => $senderName,
|
||||
'rawName' => $senderRawName,
|
||||
'contactAlias' => $senderContactAlias,
|
||||
'friendAlias' => $senderFriendAlias,
|
||||
'remarkAlias' => $senderRemarkAlias,
|
||||
'groupAlias' => $senderAlias,
|
||||
'isContact' => (bool)($senderUser['_is_friend'] ?? false),
|
||||
'mobile' => (string)($senderUser['_mobile'] ?? ''),
|
||||
'address' => (string)($senderUser['_address'] ?? ''),
|
||||
'portrait' => (string)($senderUser['_portrait'] ?? ''),
|
||||
'role' => $senderRole,
|
||||
],
|
||||
'to' => $groupId,
|
||||
'target' => $groupId,
|
||||
'direction' => $direction,
|
||||
'type' => $messageType,
|
||||
'contentType' => $contentType,
|
||||
'text' => attachmentDisplayText($text, $attachment),
|
||||
'attachment' => $attachment,
|
||||
'status' => $isRecalled && $direction === 'out' ? 'recalled' : 'sent',
|
||||
'deliveryStatus' => $direction === 'out' && !$isRecalled ? 'sent' : '',
|
||||
'recalled' => $isRecalled,
|
||||
'recallStatus' => $isRecalled ? '已撤回' : '',
|
||||
'editedAt' => 0,
|
||||
'timestamp' => $timestamp,
|
||||
'activityTimestamp' => $timestamp,
|
||||
'displayTimestamp' => $timestamp,
|
||||
'conversationType' => 'group',
|
||||
'raw' => [
|
||||
'native' => true,
|
||||
'groupName' => (string)($group['_name'] ?? ''),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function loadWebGroupMessages(PDO $appDb, string $userId, string $groupId, int $limit, bool $markRead): array
|
||||
{
|
||||
ensureWebImAppTables();
|
||||
@@ -557,6 +982,81 @@ function loadWebGroupMessages(PDO $appDb, string $userId, string $groupId, int $
|
||||
return $messages;
|
||||
}
|
||||
|
||||
function loadNativeGroupMessages(PDO $db, string $userId, string $groupId, int $limit, bool $markRead): array
|
||||
{
|
||||
$role = nativeGroupRole($db, $userId, $groupId);
|
||||
if ($role === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupStatement = $db->prepare('select * from `t_group` where `_gid` = ? and `_deleted` = 0 limit 1');
|
||||
$groupStatement->execute([$groupId]);
|
||||
$group = $groupStatement->fetch();
|
||||
if (!is_array($group)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$table = imUserMessagesTable($userId);
|
||||
$scanLimit = min(max($limit * 4, $limit), 400);
|
||||
$statement = $db->prepare(
|
||||
'select `_mid`, `_dt` from `' . $table . '`'
|
||||
. ' where `_uid` = ? and `_type` = 1 and `_line` = 0 and `_target` = ?'
|
||||
. ' order by `_mid` desc limit ' . $scanLimit
|
||||
);
|
||||
$statement->execute([$userId, $groupId]);
|
||||
$userRows = $statement->fetchAll();
|
||||
$messageRows = fetchMessageRows($db, array_column($userRows, '_mid'));
|
||||
$globalRows = loadNativeGroupRowsFromGlobal($db, [$groupId], $scanLimit);
|
||||
foreach ($globalRows as $row) {
|
||||
$messageId = (string)$row['_mid'];
|
||||
if ($messageId !== '') {
|
||||
$messageRows[$messageId] = $row;
|
||||
$userRows[] = [
|
||||
'_mid' => $messageId,
|
||||
'_dt' => (string)($row['_dt'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($userRows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid']));
|
||||
$attachmentMap = fetchAttachmentMap(imAppDb(), array_column($userRows, '_mid'));
|
||||
|
||||
$messages = [];
|
||||
$seenMessageIds = [];
|
||||
foreach ($userRows as $userRow) {
|
||||
$messageId = (string)$userRow['_mid'];
|
||||
if (isset($seenMessageIds[$messageId]) || !isset($messageRows[$messageId])) {
|
||||
continue;
|
||||
}
|
||||
$seenMessageIds[$messageId] = true;
|
||||
if (!shouldDisplayMessageForUser($messageRows[$messageId], $userId)) {
|
||||
continue;
|
||||
}
|
||||
$messages[] = $messageRows[$messageId];
|
||||
if (count($messages) >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$messages = array_reverse($messages);
|
||||
$senderIds = array_values(array_unique(array_map(static fn(array $row): string => (string)$row['_from'], $messages)));
|
||||
$users = fetchUserInfoMap($db, $senderIds, $userId);
|
||||
$memberMap = fetchNativeGroupMemberMap($db, $groupId, $senderIds);
|
||||
|
||||
$readTimestamp = 0;
|
||||
$formatted = [];
|
||||
foreach ($messages as $row) {
|
||||
$messageId = (string)$row['_mid'];
|
||||
$message = formatNativeGroupMessage($row, $userId, $group, $attachmentMap[$messageId] ?? null, $users, $memberMap);
|
||||
$readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']);
|
||||
$formatted[] = $message;
|
||||
}
|
||||
|
||||
if ($markRead) {
|
||||
markNativeGroupRead($db, $userId, $groupId, $readTimestamp);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
function loadWebGroupConversations(PDO $appDb, string $userId, int $limit): array
|
||||
{
|
||||
ensureWebImAppTables();
|
||||
@@ -619,7 +1119,7 @@ function loadWebGroupConversations(PDO $appDb, string $userId, int $limit): arra
|
||||
? (messagePreviewText([
|
||||
'text' => attachmentDisplayText((string)$lastMessage['text'], $lastAttachment),
|
||||
'attachment' => $lastAttachment,
|
||||
]) ?: (string)$lastMessage['text'])
|
||||
]))
|
||||
: ($isChannel ? '公告群' : ($memberCount > 0 ? $memberCount . ' 位成员' : '群组'));
|
||||
$updatedAt = $lastMessage
|
||||
? max((int)$lastMessage['created_at_ms'], (int)($lastMessage['edited_at_ms'] ?? 0))
|
||||
@@ -649,6 +1149,152 @@ function loadWebGroupConversations(PDO $appDb, string $userId, int $limit): arra
|
||||
return $conversations;
|
||||
}
|
||||
|
||||
function loadNativeGroupConversations(PDO $db, string $userId, int $limit): array
|
||||
{
|
||||
$statement = $db->prepare(
|
||||
'select g.*, m.`_type` as `member_type`, m.`_alias` as `member_alias`'
|
||||
. ' from `t_group_member` m'
|
||||
. ' join `t_group` g on g.`_gid` = m.`_gid` and g.`_deleted` = 0'
|
||||
. ' where m.`_mid` = ?'
|
||||
. ' order by greatest(g.`_dt`, m.`_dt`) desc limit ' . min(max($limit, 50), 100)
|
||||
);
|
||||
$statement->execute([$userId]);
|
||||
$groups = $statement->fetchAll();
|
||||
if (!$groups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupIds = array_values(array_unique(array_map(static fn(array $group): string => (string)$group['_gid'], $groups)));
|
||||
$table = imUserMessagesTable($userId);
|
||||
$scanLimit = min(max($limit * 20, 200), 1000);
|
||||
$messageStatement = $db->prepare(
|
||||
'select `_mid`, `_target`, `_dt` from `' . $table . '`'
|
||||
. ' where `_uid` = ? and `_type` = 1 and `_line` = 0'
|
||||
. ' and `_target` in (' . placeholders($groupIds) . ')'
|
||||
. ' order by `_mid` desc limit ' . $scanLimit
|
||||
);
|
||||
$messageStatement->execute(array_merge([$userId], $groupIds));
|
||||
$scanRows = $messageStatement->fetchAll();
|
||||
$messageRows = fetchMessageRows($db, array_column($scanRows, '_mid'));
|
||||
$globalRows = loadNativeGroupRowsFromGlobal($db, $groupIds, $scanLimit);
|
||||
foreach ($globalRows as $row) {
|
||||
$messageId = (string)$row['_mid'];
|
||||
if ($messageId !== '') {
|
||||
$messageRows[$messageId] = $row;
|
||||
$scanRows[] = [
|
||||
'_mid' => $messageId,
|
||||
'_target' => (string)($row['_target'] ?? ''),
|
||||
'_dt' => (string)($row['_dt'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
usort($scanRows, static fn($left, $right) => strcmp((string)$right['_mid'], (string)$left['_mid']));
|
||||
$attachmentMap = fetchAttachmentMap(imAppDb(), array_column($scanRows, '_mid'));
|
||||
$readAtByGroup = loadNativeGroupReadStateMap($db, $userId, $groupIds);
|
||||
|
||||
$latestByGroup = [];
|
||||
$unreadByGroup = [];
|
||||
$seenMessageIds = [];
|
||||
foreach ($scanRows as $row) {
|
||||
$messageId = (string)$row['_mid'];
|
||||
if (isset($seenMessageIds[$messageId])) {
|
||||
continue;
|
||||
}
|
||||
$seenMessageIds[$messageId] = true;
|
||||
$groupId = (string)$row['_target'];
|
||||
$message = $messageRows[$messageId] ?? null;
|
||||
if ($groupId === '' || !$message || !shouldDisplayMessageForUser($message, $userId)) {
|
||||
continue;
|
||||
}
|
||||
$activityTimestamp = timestampMs((string)($message['_dt'] ?? ''), $messageId);
|
||||
if (!isset($latestByGroup[$groupId]) || $activityTimestamp > (int)($latestByGroup[$groupId]['_activity_at'] ?? 0)) {
|
||||
$row['_activity_at'] = $activityTimestamp;
|
||||
$latestByGroup[$groupId] = $row;
|
||||
}
|
||||
if ((string)$message['_from'] !== $userId && !isRecallMessage($message)) {
|
||||
if ($activityTimestamp > (int)($readAtByGroup[$groupId] ?? 0)) {
|
||||
$unreadByGroup[$groupId] = (int)($unreadByGroup[$groupId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$groupsById = [];
|
||||
foreach ($groups as $group) {
|
||||
$groupsById[(string)$group['_gid']] = $group;
|
||||
}
|
||||
|
||||
$senderIds = [];
|
||||
foreach ($latestByGroup as $row) {
|
||||
$message = $messageRows[(string)$row['_mid']] ?? null;
|
||||
if ($message) {
|
||||
$senderIds[] = (string)$message['_from'];
|
||||
}
|
||||
}
|
||||
$users = fetchUserInfoMap($db, $senderIds, $userId);
|
||||
|
||||
$conversations = [];
|
||||
foreach ($groupsById as $groupId => $group) {
|
||||
$latestRow = $latestByGroup[$groupId] ?? null;
|
||||
$messageId = $latestRow ? (string)$latestRow['_mid'] : '';
|
||||
$message = $messageId !== '' && isset($messageRows[$messageId])
|
||||
? formatNativeGroupMessage(
|
||||
$messageRows[$messageId],
|
||||
$userId,
|
||||
$group,
|
||||
$attachmentMap[$messageId] ?? null,
|
||||
$users,
|
||||
fetchNativeGroupMemberMap($db, $groupId, [(string)$messageRows[$messageId]['_from']])
|
||||
)
|
||||
: null;
|
||||
$title = (string)($group['_name'] ?? '') ?: $groupId;
|
||||
$updatedAt = $message ? (int)$message['activityTimestamp'] : (int)($group['_dt'] ?? 0);
|
||||
$conversations[] = [
|
||||
'id' => 'group-' . $groupId,
|
||||
'type' => 'group',
|
||||
'target' => $groupId,
|
||||
'title' => $title,
|
||||
'subtitle' => $message
|
||||
? (messagePreviewText($message) ?: nativeMessageFallbackText((int)($message['contentType'] ?? 0), false))
|
||||
: (((int)($group['_member_count'] ?? 0) > 0) ? (int)$group['_member_count'] . ' 位成员' : '群组'),
|
||||
'unread' => (int)($unreadByGroup[$groupId] ?? 0),
|
||||
'portrait' => (string)($group['_portrait'] ?? ''),
|
||||
'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'),
|
||||
'updatedAt' => $updatedAt,
|
||||
'role' => nativeGroupRoleFromType($group['member_type'] ?? 0),
|
||||
'memberCount' => (int)($group['_member_count'] ?? 0),
|
||||
'raw' => [
|
||||
'type' => 'group',
|
||||
'target' => $groupId,
|
||||
'line' => 0,
|
||||
'native' => true,
|
||||
'role' => nativeGroupRoleFromType($group['member_type'] ?? 0),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $conversations;
|
||||
}
|
||||
|
||||
function mergeConversationsById(array $conversations, array $extraConversations): array
|
||||
{
|
||||
$merged = [];
|
||||
foreach (array_merge($conversations, $extraConversations) as $conversation) {
|
||||
$id = (string)($conversation['id'] ?? '');
|
||||
if ($id === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($merged[$id])) {
|
||||
$merged[$id] = $conversation;
|
||||
continue;
|
||||
}
|
||||
if ((int)($conversation['updatedAt'] ?? 0) > (int)($merged[$id]['updatedAt'] ?? 0)) {
|
||||
$merged[$id] = array_merge($merged[$id], $conversation);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($merged);
|
||||
}
|
||||
|
||||
function isRecallMessage(array $row): bool
|
||||
{
|
||||
return (int)($row['_content_type'] ?? 0) === 80;
|
||||
@@ -679,8 +1325,9 @@ function formatMessage(
|
||||
$text = (string)($row['_searchable_key'] ?? '');
|
||||
$timestamp = timestampMs((string)($row['_dt'] ?? ''), $messageId);
|
||||
$direction = $from === $currentUserId ? 'out' : 'in';
|
||||
$attachment = nativeAttachmentFromRow($row, $attachment);
|
||||
$messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown');
|
||||
$messageText = $text !== '' ? $text : ($isRecalled || $attachment ? '' : '[暂不支持的消息类型]');
|
||||
$messageText = nativeDisplayText($text, $contentType, $attachment !== null, $isRecalled);
|
||||
$messageText = attachmentDisplayText($messageText, $attachment);
|
||||
$editedAt = (int)($editState['editedAt'] ?? 0);
|
||||
$activityTimestamp = messageActivityTimestamp($timestamp, $editState);
|
||||
@@ -854,10 +1501,16 @@ function loadRecentConversations(PDO $db, string $userId, int $limit): array
|
||||
}
|
||||
|
||||
try {
|
||||
$conversations = array_merge($conversations, loadWebGroupConversations(imAppDb(), $userId, $limit));
|
||||
} catch (Throwable) {
|
||||
$conversations = mergeConversationsById($conversations, loadWebGroupConversations(imAppDb(), $userId, $limit));
|
||||
} catch (Throwable $error) {
|
||||
error_log('web group conversation sync failed: ' . $error->getMessage());
|
||||
// Keep single-chat sync available if appdata group metadata is temporarily unavailable.
|
||||
}
|
||||
try {
|
||||
$conversations = mergeConversationsById($conversations, loadNativeGroupConversations($db, $userId, $limit));
|
||||
} catch (Throwable $error) {
|
||||
error_log('native group conversation sync failed: ' . $error->getMessage());
|
||||
}
|
||||
|
||||
usort($conversations, static fn($a, $b) => $b['updatedAt'] <=> $a['updatedAt']);
|
||||
|
||||
@@ -881,12 +1534,27 @@ try {
|
||||
markUserDelivered($db, $userId, nowMs());
|
||||
|
||||
if ($target !== '') {
|
||||
$targetId = resolveUserId($db, $target);
|
||||
if ($conversationType === 'group' || isWebGroupTarget(imAppDb(), $target)) {
|
||||
if ($conversationType === 'group') {
|
||||
if (isWebGroupTarget(imAppDb(), $target)) {
|
||||
respond(0, 'success', [
|
||||
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
|
||||
]);
|
||||
}
|
||||
if (isNativeGroupTarget($db, $target)) {
|
||||
respond(0, 'success', [
|
||||
'messages' => loadNativeGroupMessages($db, $userId, $target, $limit, $markRead),
|
||||
]);
|
||||
}
|
||||
respond(0, 'success', [
|
||||
'messages' => [],
|
||||
]);
|
||||
}
|
||||
if (isWebGroupTarget(imAppDb(), $target)) {
|
||||
respond(0, 'success', [
|
||||
'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead),
|
||||
]);
|
||||
}
|
||||
$targetId = resolveUserId($db, $target);
|
||||
respond(0, 'success', [
|
||||
'messages' => loadConversationMessages($db, $userId, $targetId, $limit, $markRead),
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user