270 lines
6.6 KiB
TypeScript
270 lines
6.6 KiB
TypeScript
|
|
/**
|
|||
|
|
* 消息气泡样式工具
|
|||
|
|
* 参考 Element X 设计,实现动态圆角和现代化气泡样式
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { StyleSheet, ViewStyle, TextStyle } from 'react-native';
|
|||
|
|
import { colors, spacing } from '../../../../theme';
|
|||
|
|
|
|||
|
|
// 气泡圆角大小(参考 Element X: 12pt)
|
|||
|
|
export const BUBBLE_RADIUS = 16;
|
|||
|
|
|
|||
|
|
// 气泡颜色方案(参考 Element X 的语义化颜色)
|
|||
|
|
export const bubbleColors = {
|
|||
|
|
// 自己发送的消息
|
|||
|
|
outgoing: {
|
|||
|
|
background: '#E8E8E8', // Element X 风格:灰色系
|
|||
|
|
text: '#1A1A1A',
|
|||
|
|
},
|
|||
|
|
// 收到的消息
|
|||
|
|
incoming: {
|
|||
|
|
background: '#FFFFFF',
|
|||
|
|
text: '#1A1A1A',
|
|||
|
|
},
|
|||
|
|
// 被回复的消息高亮
|
|||
|
|
replyHighlight: {
|
|||
|
|
background: 'rgba(255, 107, 53, 0.08)',
|
|||
|
|
borderLeft: '#FF6B35',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 消息在组中的位置类型
|
|||
|
|
export type MessageGroupPosition = 'single' | 'first' | 'middle' | 'last';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 根据消息在组中的位置获取圆角样式
|
|||
|
|
* 参考 Element X 的动态圆角逻辑
|
|||
|
|
*/
|
|||
|
|
export const getBubbleBorderRadius = (
|
|||
|
|
isMe: boolean,
|
|||
|
|
position: MessageGroupPosition
|
|||
|
|
): ViewStyle => {
|
|||
|
|
const radius = BUBBLE_RADIUS;
|
|||
|
|
|
|||
|
|
if (isMe) {
|
|||
|
|
// 自己发送的消息
|
|||
|
|
switch (position) {
|
|||
|
|
case 'single':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: radius,
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: 4, // 尖角在右下
|
|||
|
|
};
|
|||
|
|
case 'first':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: radius,
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: 4,
|
|||
|
|
};
|
|||
|
|
case 'middle':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: radius,
|
|||
|
|
borderTopRightRadius: 4,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: 4,
|
|||
|
|
};
|
|||
|
|
case 'last':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: radius,
|
|||
|
|
borderTopRightRadius: 4,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: radius,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 收到的消息
|
|||
|
|
switch (position) {
|
|||
|
|
case 'single':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: 4, // 尖角在左上
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: radius,
|
|||
|
|
};
|
|||
|
|
case 'first':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: 4,
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: 4,
|
|||
|
|
borderBottomRightRadius: radius,
|
|||
|
|
};
|
|||
|
|
case 'middle':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: 4,
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: 4,
|
|||
|
|
borderBottomRightRadius: radius,
|
|||
|
|
};
|
|||
|
|
case 'last':
|
|||
|
|
return {
|
|||
|
|
borderTopLeftRadius: radius,
|
|||
|
|
borderTopRightRadius: radius,
|
|||
|
|
borderBottomLeftRadius: radius,
|
|||
|
|
borderBottomRightRadius: radius,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 判断消息在组中的位置
|
|||
|
|
*/
|
|||
|
|
export const getMessageGroupPosition = (
|
|||
|
|
index: number,
|
|||
|
|
messages: Array<{ sender_id: string }>,
|
|||
|
|
currentUserId: string
|
|||
|
|
): MessageGroupPosition => {
|
|||
|
|
const currentMessage = messages[index];
|
|||
|
|
if (!currentMessage) return 'single';
|
|||
|
|
|
|||
|
|
const prevMessage = index > 0 ? messages[index - 1] : null;
|
|||
|
|
const nextMessage = index < messages.length - 1 ? messages[index + 1] : null;
|
|||
|
|
|
|||
|
|
const isSameSenderAsPrev = prevMessage && prevMessage.sender_id === currentMessage.sender_id;
|
|||
|
|
const isSameSenderAsNext = nextMessage && nextMessage.sender_id === currentMessage.sender_id;
|
|||
|
|
|
|||
|
|
if (!isSameSenderAsPrev && !isSameSenderAsNext) {
|
|||
|
|
return 'single';
|
|||
|
|
} else if (!isSameSenderAsPrev && isSameSenderAsNext) {
|
|||
|
|
return 'first';
|
|||
|
|
} else if (isSameSenderAsPrev && isSameSenderAsNext) {
|
|||
|
|
return 'middle';
|
|||
|
|
} else {
|
|||
|
|
return 'last';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 判断是否显示发送者信息(只在组的第一条消息显示)
|
|||
|
|
*/
|
|||
|
|
export const shouldShowSenderInfo = (
|
|||
|
|
index: number,
|
|||
|
|
messages: Array<{ sender_id: string }>,
|
|||
|
|
): boolean => {
|
|||
|
|
if (index === 0) return true;
|
|||
|
|
|
|||
|
|
const currentMessage = messages[index];
|
|||
|
|
const prevMessage = messages[index - 1];
|
|||
|
|
|
|||
|
|
return prevMessage.sender_id !== currentMessage.sender_id;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取气泡基础样式
|
|||
|
|
*/
|
|||
|
|
export const getBubbleBaseStyle = (isMe: boolean): ViewStyle => ({
|
|||
|
|
paddingHorizontal: spacing.md,
|
|||
|
|
paddingVertical: spacing.sm + 4,
|
|||
|
|
minWidth: 60,
|
|||
|
|
maxWidth: '75%',
|
|||
|
|
backgroundColor: isMe ? bubbleColors.outgoing.background : bubbleColors.incoming.background,
|
|||
|
|
// Element X 风格的微妙阴影
|
|||
|
|
shadowColor: '#000',
|
|||
|
|
shadowOffset: { width: 0, height: 1 },
|
|||
|
|
shadowOpacity: 0.06,
|
|||
|
|
shadowRadius: 2,
|
|||
|
|
elevation: 2,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取文本样式
|
|||
|
|
*/
|
|||
|
|
export const getBubbleTextStyle = (isMe: boolean): TextStyle => ({
|
|||
|
|
color: isMe ? bubbleColors.outgoing.text : bubbleColors.incoming.text,
|
|||
|
|
fontSize: 16,
|
|||
|
|
lineHeight: 23,
|
|||
|
|
letterSpacing: 0.2,
|
|||
|
|
fontWeight: '400',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 消息气泡样式集合
|
|||
|
|
*/
|
|||
|
|
export const bubbleStyles = StyleSheet.create({
|
|||
|
|
// 基础气泡
|
|||
|
|
bubble: {
|
|||
|
|
paddingHorizontal: spacing.md,
|
|||
|
|
paddingVertical: spacing.sm + 4,
|
|||
|
|
minWidth: 60,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 自己发送的消息
|
|||
|
|
outgoing: {
|
|||
|
|
backgroundColor: bubbleColors.outgoing.background,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 收到的消息
|
|||
|
|
incoming: {
|
|||
|
|
backgroundColor: bubbleColors.incoming.background,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 文本样式
|
|||
|
|
text: {
|
|||
|
|
fontSize: 16,
|
|||
|
|
lineHeight: 23,
|
|||
|
|
letterSpacing: 0.2,
|
|||
|
|
fontWeight: '400',
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 阴影样式(Element X 风格)
|
|||
|
|
shadow: {
|
|||
|
|
shadowColor: '#000',
|
|||
|
|
shadowOffset: { width: 0, height: 1 },
|
|||
|
|
shadowOpacity: 0.06,
|
|||
|
|
shadowRadius: 2,
|
|||
|
|
elevation: 2,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 长按反馈阴影
|
|||
|
|
longPressShadow: {
|
|||
|
|
shadowColor: '#000',
|
|||
|
|
shadowOffset: { width: 0, height: 3 },
|
|||
|
|
shadowOpacity: 0.12,
|
|||
|
|
shadowRadius: 8,
|
|||
|
|
elevation: 6,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 被回复消息的高亮边框
|
|||
|
|
replyHighlight: {
|
|||
|
|
borderLeftWidth: 3,
|
|||
|
|
borderLeftColor: colors.primary.main,
|
|||
|
|
backgroundColor: bubbleColors.replyHighlight.background,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 已撤回消息
|
|||
|
|
recalled: {
|
|||
|
|
backgroundColor: '#F5F7FA',
|
|||
|
|
borderWidth: 1,
|
|||
|
|
borderColor: '#E8E8E8',
|
|||
|
|
borderStyle: 'dashed',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 系统通知
|
|||
|
|
systemNotice: {
|
|||
|
|
alignItems: 'center',
|
|||
|
|
marginVertical: spacing.sm,
|
|||
|
|
},
|
|||
|
|
systemNoticeText: {
|
|||
|
|
fontSize: 12,
|
|||
|
|
color: '#8E8E93',
|
|||
|
|
backgroundColor: 'rgba(142, 142, 147, 0.12)',
|
|||
|
|
paddingHorizontal: spacing.md,
|
|||
|
|
paddingVertical: spacing.xs,
|
|||
|
|
borderRadius: 12,
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
BUBBLE_RADIUS,
|
|||
|
|
bubbleColors,
|
|||
|
|
getBubbleBorderRadius,
|
|||
|
|
getMessageGroupPosition,
|
|||
|
|
shouldShowSenderInfo,
|
|||
|
|
getBubbleBaseStyle,
|
|||
|
|
getBubbleTextStyle,
|
|||
|
|
bubbleStyles,
|
|||
|
|
};
|