feat(push): integrate Honor and Xiaomi push plugins with related updates

This commit is contained in:
lafay
2026-06-18 00:03:37 +08:00
parent b2979311bb
commit 96e8de18bf
20 changed files with 1088 additions and 265 deletions

View File

@@ -476,23 +476,102 @@ const renderVideoSegment = (data: VideoSegmentData, isMe: boolean): React.ReactN
return <VideoSegment key={`video-${data.url}`} data={data} isMe={isMe} />;
};
/**
* 根据 MIME 类型返回文件图标名与主题色
*/
function getFileVisual(mime?: string): { icon: string; color: string } {
if (!mime) return { icon: 'file-document-outline', color: '#5C6BC0' };
const m = mime.toLowerCase();
if (m === 'application/pdf') return { icon: 'file-pdf-box', color: '#E53935' };
if (m.includes('word') || m === 'application/msword') return { icon: 'file-word-box', color: '#1E88E5' };
if (m.includes('excel') || m === 'application/vnd.ms-excel' || m === 'text/csv') return { icon: 'file-excel-box', color: '#43A047' };
if (m.includes('powerpoint') || m === 'application/vnd.ms-powerpoint') return { icon: 'file-powerpoint-box', color: '#FB8C00' };
if (m === 'application/zip' || m.includes('compressed') || m.includes('rar') || m.includes('7z') || m.includes('gzip') || m.includes('tar')) {
return { icon: 'folder-zip-outline', color: '#8D6E63' };
}
if (m.startsWith('audio/')) return { icon: 'file-music-outline', color: '#8E24AA' };
if (m.startsWith('video/')) return { icon: 'file-video-outline', color: '#00897B' };
if (m.startsWith('image/')) return { icon: 'file-image-outline', color: '#FF6B35' };
if (m === 'application/json' || m === 'text/xml' || m === 'application/xml') return { icon: 'code-json', color: '#455A64' };
if (m === 'text/plain' || m === 'text/markdown') return { icon: 'file-document-outline', color: '#607D8B' };
return { icon: 'file-document-outline', color: '#5C6BC0' };
}
/**
* 打开/下载文件:移动端用浏览器在新页面打开 URL系统会提示下载/预览),
* 此处不再依赖 expo-file-systemSDK 56 已废弃 downloadAsync
*/
async function openRemoteFile(url: string, name: string) {
if (!url) return;
try {
await Linking.openURL(url);
} catch (error) {
console.warn('打开文件失败:', error);
}
}
/**
* 渲染文件 Segment
* 当 data.expired 为 true 时显示"文件已过期"失效态,禁用点击。
*/
const FileSegmentBody: React.FC<{ data: FileSegmentData; isMe: boolean }> = ({ data, isMe }) => {
const styles = useSegmentStyles();
const themeColors = useAppColors();
const [downloading, setDownloading] = useState(false);
const fileSize = data.size ? formatFileSize(data.size) : '';
const visual = getFileVisual(data.mime_type);
const isExpired = !!data.expired;
const handlePress = async () => {
if (isExpired || !data.url || downloading) return;
setDownloading(true);
try {
await openRemoteFile(data.url, data.name || 'file');
} finally {
setDownloading(false);
}
};
// 失效态:灰态卡片 + 时钟图标 + "文件已过期",禁用点击
if (isExpired) {
return (
<View
style={[styles.fileContainer, styles.fileExpired, isMe ? styles.fileMe : styles.fileOther]}
pointerEvents="none"
>
<View style={[styles.fileIcon, { backgroundColor: 'rgba(150,150,150,0.15)' }]}>
<MaterialCommunityIcons
name="clock-alert-outline"
size={26}
color={themeColors.chat.textTertiary}
/>
</View>
<View style={styles.fileInfo}>
<Text
style={[styles.fileName, styles.fileExpiredText]}
numberOfLines={1}
>
{data.name}
</Text>
<Text style={[styles.fileSize, styles.fileExpiredText]}>
</Text>
</View>
</View>
);
}
return (
<TouchableOpacity
style={[styles.fileContainer, isMe ? styles.fileMe : styles.fileOther]}
onPress={handlePress}
activeOpacity={0.7}
>
<View style={styles.fileIcon}>
<View style={[styles.fileIcon, { backgroundColor: `${visual.color}22` }]}>
<MaterialCommunityIcons
name="file-document"
size={28}
color={isMe ? themeColors.primary.contrast : themeColors.primary.main}
name={visual.icon as any}
size={26}
color={isMe ? themeColors.primary.contrast : visual.color}
/>
</View>
<View style={styles.fileInfo}>
@@ -502,8 +581,15 @@ const FileSegmentBody: React.FC<{ data: FileSegmentData; isMe: boolean }> = ({ d
>
{data.name}
</Text>
{fileSize ? <Text style={styles.fileSize}>{fileSize}</Text> : null}
<Text style={styles.fileSize}>
{downloading ? '下载中…' : (fileSize || (data.mime_type || '文件'))}
</Text>
</View>
<MaterialCommunityIcons
name={downloading ? 'progress-download' : 'download-outline'}
size={20}
color={isMe ? themeColors.primary.contrast : themeColors.chat.textTertiary}
/>
</TouchableOpacity>
);
};
@@ -1021,6 +1107,14 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
fontWeight: '500',
},
// 文件过期失效态
fileExpired: {
opacity: 0.65,
},
fileExpiredText: {
color: colors.chat.textTertiary,
},
// 链接 - QQ风格卡片式设计
linkContainer: {
borderRadius: 16,

View File

@@ -171,20 +171,13 @@ export const MORE_ACTIONS: MoreAction[] = [
color: '#26A69A',
gradientColors: ['#4DB6AC', '#00897B'],
},
{
id: 'file',
icon: 'file-document',
name: '文件',
{
id: 'file',
icon: 'file-document',
name: '文件',
color: '#5C6BC0',
gradientColors: ['#7986CB', '#3F51B5'],
},
{
id: 'location',
icon: 'map-marker',
name: '位置',
color: '#EC407A',
gradientColors: ['#F06292', '#D81B60'],
},
];
// 消息撤回时间限制(毫秒)

View File

@@ -20,7 +20,9 @@ import {
import { useLocalSearchParams, router } from 'expo-router';
import { formatChatTime } from '@/utils/formatTime';
import * as ImagePicker from 'expo-image-picker';
import { GroupMemberResponse, MessageSegment, TextSegmentData, ImageSegmentData, AtSegmentData, ReplySegmentData, MessageStatus } from '../../../../types/dto';
import * as DocumentPicker from 'expo-document-picker';
import { Linking } from 'react-native';
import { GroupMemberResponse, MessageSegment, TextSegmentData, ImageSegmentData, FileSegmentData, AtSegmentData, ReplySegmentData, MessageStatus } from '../../../../types/dto';
import { messageService } from '@/services/message';
import { uploadService } from '@/services/upload';
import { ApiError } from '@/services/core';
@@ -1049,6 +1051,38 @@ export const useChatScreen = (props?: ChatScreenProps) => {
return segments;
}, []);
/**
* 构建文件消息的 segments 数组
*/
const buildFileSegments = useCallback((
fileUrl: string,
name: string,
size?: number,
mimeType?: string,
replyToMessage?: GroupMessage | null,
): MessageSegment[] => {
const segments: MessageSegment[] = [];
if (replyToMessage) {
segments.push({
type: 'reply',
data: { id: replyToMessage.id, seq: replyToMessage.seq } as ReplySegmentData
});
}
segments.push({
type: 'file',
data: {
url: fileUrl,
name,
size,
mime_type: mimeType,
} as FileSegmentData
});
return segments;
}, []);
// 【新架构】发送消息(支持纯文字、纯多图、图文同条)
const handleSend = useCallback(async () => {
const trimmedText = inputText.trim();
@@ -1253,6 +1287,89 @@ export const useChatScreen = (props?: ChatScreenProps) => {
setActivePanel('none');
}, [pendingAttachments.length]);
// 选择并发送文件(单选,立即上传后发送)
const handlePickFile = useCallback(async () => {
if (!conversationId) {
setActivePanel('none');
return;
}
if (isGroupChat && isMuted) {
Alert.alert('无法发送', muteAll ? '当前群组已开启全员禁言' : '你已被管理员禁言');
setActivePanel('none');
return;
}
setActivePanel('none');
try {
const result = await DocumentPicker.getDocumentAsync({
multiple: false,
copyToCacheDirectory: true,
});
if (result.canceled || !result.assets || result.assets.length === 0) {
return;
}
const asset = result.assets[0];
setUploadingAttachments(true);
const uploaded = await uploadService.uploadFile(
{
uri: asset.uri,
name: asset.name,
type: asset.mimeType || undefined,
},
'chat'
);
if (!uploaded?.url) {
Alert.alert('上传失败', getSendErrorMessage(null, '文件上传失败,请重试'));
return;
}
const segments = buildFileSegments(
uploaded.url,
uploaded.name || asset.name,
uploaded.size ?? asset.size,
uploaded.mime_type ?? asset.mimeType,
replyingTo,
);
setSending(true);
try {
if (isGroupChat && effectiveGroupId) {
await messageService.sendMessageByAction('group', conversationId, segments);
} else {
await sendMessageViaManager(segments);
}
setReplyingTo(null);
setTimeout(() => scrollToLatest(false, false, 'send-file'), 100);
} catch (error) {
console.error('发送文件消息失败:', error);
Alert.alert('发送失败', getSendErrorMessage(error, '消息发送失败,请重试'));
} finally {
setSending(false);
}
} catch (error) {
console.error('选择文件失败:', error);
Alert.alert('错误', getSendErrorMessage(error, '选择文件失败'));
} finally {
setUploadingAttachments(false);
}
}, [
conversationId,
isGroupChat,
isMuted,
muteAll,
effectiveGroupId,
replyingTo,
buildFileSegments,
sendMessageViaManager,
scrollToLatest,
getSendErrorMessage,
]);
// 处理更多功能
const handleMoreAction = useCallback((actionId: string) => {
switch (actionId) {
@@ -1281,17 +1398,12 @@ export const useChatScreen = (props?: ChatScreenProps) => {
handleTakePhoto();
break;
case 'file':
Alert.alert('提示', '文件功能即将上线');
setActivePanel('none');
break;
case 'location':
Alert.alert('提示', '位置功能即将上线');
setActivePanel('none');
handlePickFile();
break;
default:
setActivePanel('none');
}
}, [handlePickImage, handleTakePhoto, isGroupChat, otherUser, conversationId]);
}, [handlePickImage, handleTakePhoto, handlePickFile, isGroupChat, otherUser, conversationId]);
// 插入表情
const handleInsertEmoji = useCallback((emoji: string) => {