mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
chore(matrix-mobile): configure EAS Build for TestFlight and fix type errors
- Update to Expo SDK 53 compatible dependencies (from speculative SDK 55) - Add EAS Build config (eas.json) with dev/preview/production profiles - Generate app icons and splash screen assets - Add NativeWind type augmentation for Pressable className callbacks - Fix matrix-js-sdk NotificationCountType enum usage in store - Fix Swipeable render actions type mismatch with Reanimated v3 - Exclude svelte dirs from wallpaper-generator and qr-export tsconfigs - Remove duplicate scripts in root package.json - Update pnpm-lock.yaml with @matrix/mobile dependencies Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a9c05ca46b
commit
bc3a527bf4
16 changed files with 2555 additions and 694 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Manalink",
|
||||
"slug": "manalink",
|
||||
"owner": "tilljs",
|
||||
"version": "1.0.0",
|
||||
"scheme": "manalink",
|
||||
"orientation": "portrait",
|
||||
|
|
@ -25,7 +26,13 @@
|
|||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#0f0f0f"
|
||||
},
|
||||
"package": "how.mana.manalink"
|
||||
"package": "how.mana.manalink",
|
||||
"permissions": [
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.MODIFY_AUDIO_SETTINGS",
|
||||
"android.permission.READ_EXTERNAL_STORAGE",
|
||||
"android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
|
|
@ -35,6 +42,7 @@
|
|||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store",
|
||||
"expo-av",
|
||||
[
|
||||
"expo-image-picker",
|
||||
{
|
||||
|
|
@ -67,7 +75,7 @@
|
|||
"origin": false
|
||||
},
|
||||
"eas": {
|
||||
"projectId": ""
|
||||
"projectId": "a4c5098c-fcae-474e-95b2-13394d8b323d"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
apps/matrix/apps/mobile/assets/adaptive-icon.png
Normal file
BIN
apps/matrix/apps/mobile/assets/adaptive-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
BIN
apps/matrix/apps/mobile/assets/favicon.png
Normal file
BIN
apps/matrix/apps/mobile/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 955 B |
BIN
apps/matrix/apps/mobile/assets/icon.png
Normal file
BIN
apps/matrix/apps/mobile/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
BIN
apps/matrix/apps/mobile/assets/notification-icon.png
Normal file
BIN
apps/matrix/apps/mobile/assets/notification-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
apps/matrix/apps/mobile/assets/splash.png
Normal file
BIN
apps/matrix/apps/mobile/assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
|
|
@ -4,15 +4,33 @@
|
|||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"base": {
|
||||
"node": "22.22.1",
|
||||
"pnpm": {
|
||||
"version": "9.15.0"
|
||||
},
|
||||
"env": {
|
||||
"EAS_BUILD_RUNNER": "eas-build-on-success"
|
||||
}
|
||||
},
|
||||
"development": {
|
||||
"extends": "base",
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
"extends": "base",
|
||||
"distribution": "internal",
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
"extends": "base",
|
||||
"autoIncrement": true,
|
||||
"ios": {
|
||||
"image": "latest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
|
|
|
|||
9
apps/matrix/apps/mobile/nativewind-env.d.ts
vendored
9
apps/matrix/apps/mobile/nativewind-env.d.ts
vendored
|
|
@ -1 +1,10 @@
|
|||
/// <reference types="nativewind/types" />
|
||||
|
||||
import 'react-native';
|
||||
|
||||
declare module 'react-native' {
|
||||
interface PressableProps {
|
||||
className?: string | ((state: { pressed: boolean }) => string);
|
||||
cssInterop?: boolean;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,42 +6,44 @@
|
|||
"dev": "expo start",
|
||||
"ios": "expo run:ios",
|
||||
"android": "expo run:android",
|
||||
"build:dev": "eas build --profile development",
|
||||
"build:preview": "eas build --profile preview",
|
||||
"build:prod": "eas build --profile production",
|
||||
"build:dev": "eas build --profile development --platform ios",
|
||||
"build:preview": "eas build --profile preview --platform ios",
|
||||
"build:prod": "eas build --profile production --platform ios",
|
||||
"submit:ios": "eas submit --platform ios --profile production",
|
||||
"build:testflight": "eas build --profile production --platform ios --auto-submit",
|
||||
"prebuild": "expo prebuild",
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"expo": "^55.0.0",
|
||||
"expo-router": "~7.0.0",
|
||||
"expo-constants": "~19.0.0",
|
||||
"expo-status-bar": "~4.0.0",
|
||||
"expo-system-ui": "~7.0.0",
|
||||
"expo-linking": "~9.0.0",
|
||||
"expo-secure-store": "~16.0.0",
|
||||
"expo-image": "~4.0.0",
|
||||
"expo-image-picker": "~16.0.0",
|
||||
"expo-document-picker": "~13.0.0",
|
||||
"expo-media-library": "~17.0.0",
|
||||
"expo-file-system": "~19.0.0",
|
||||
"expo-av": "~15.0.0",
|
||||
"expo-notifications": "~1.0.0",
|
||||
"expo-haptics": "~16.0.0",
|
||||
"expo-web-browser": "~16.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "~0.82.0",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.2.0",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.17.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"nativewind": "latest",
|
||||
"expo": "~53.0.0",
|
||||
"expo-router": "~5.0.0",
|
||||
"expo-constants": "~17.1.0",
|
||||
"expo-status-bar": "~2.2.0",
|
||||
"expo-system-ui": "~5.0.0",
|
||||
"expo-linking": "~7.1.0",
|
||||
"expo-secure-store": "~14.2.0",
|
||||
"expo-image": "~2.3.0",
|
||||
"expo-image-picker": "~16.1.0",
|
||||
"expo-document-picker": "~13.1.0",
|
||||
"expo-media-library": "~17.1.0",
|
||||
"expo-file-system": "~18.1.0",
|
||||
"expo-av": "~15.1.0",
|
||||
"expo-notifications": "~0.31.0",
|
||||
"expo-haptics": "~14.1.0",
|
||||
"expo-web-browser": "~14.1.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-reanimated": "~3.17.0",
|
||||
"react-native-safe-area-context": "~5.4.0",
|
||||
"react-native-screens": "~4.10.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"nativewind": "~4.1.0",
|
||||
"zustand": "^4.5.1",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"matrix-js-sdk": "^37.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
|
|
@ -50,8 +52,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"@types/react-native": "~0.82.0",
|
||||
"@types/react": "~18.3.0",
|
||||
"eslint": "^9.25.1",
|
||||
"eslint-config-expo": "^9.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
import { useState } from 'react';
|
||||
import { View, Text, Pressable, ActionSheetIOS, Platform, Alert, Clipboard, Modal, ScrollView } from 'react-native';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
ActionSheetIOS,
|
||||
Platform,
|
||||
Alert,
|
||||
Clipboard,
|
||||
Modal,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated';
|
||||
import { Image } from 'expo-image';
|
||||
|
|
@ -24,7 +34,17 @@ function formatTime(ts: number) {
|
|||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function AvatarCircle({ name, url, onPress, size = 28 }: { name: string; url?: string; onPress?: () => void; size?: number }) {
|
||||
function AvatarCircle({
|
||||
name,
|
||||
url,
|
||||
onPress,
|
||||
size = 28,
|
||||
}: {
|
||||
name: string;
|
||||
url?: string;
|
||||
onPress?: () => void;
|
||||
size?: number;
|
||||
}) {
|
||||
const inner = (
|
||||
<View
|
||||
style={{ width: size, height: size, borderRadius: size / 2 }}
|
||||
|
|
@ -56,37 +76,66 @@ function SwipeReplyAction({ progress }: { progress: Animated.SharedValue<number>
|
|||
}));
|
||||
return (
|
||||
<View className="justify-center items-center w-16">
|
||||
<Animated.View style={style} className="w-9 h-9 rounded-full bg-primary/20 items-center justify-center">
|
||||
<Animated.View
|
||||
style={style}
|
||||
className="w-9 h-9 rounded-full bg-primary/20 items-center justify-center"
|
||||
>
|
||||
<ArrowBendUpLeft size={18} color="#7c6bff" />
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function ReactionDetailsModal({ reactions, visible, onClose }: { reactions: MessageReaction[]; visible: boolean; onClose: () => void }) {
|
||||
function ReactionDetailsModal({
|
||||
reactions,
|
||||
visible,
|
||||
onClose,
|
||||
}: {
|
||||
reactions: MessageReaction[];
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [selectedKey, setSelectedKey] = useState<string | null>(null);
|
||||
const selected = selectedKey ? reactions.find((r) => r.key === selectedKey) : reactions[0];
|
||||
|
||||
return (
|
||||
<Modal visible={visible} animationType="slide" presentationStyle="formSheet" onRequestClose={onClose}>
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
presentationStyle="formSheet"
|
||||
onRequestClose={onClose}
|
||||
>
|
||||
<View className="flex-1 bg-background">
|
||||
<View className="flex-row items-center justify-between px-4 py-3 border-b border-border">
|
||||
<Text className="text-foreground text-lg font-semibold">Reactions</Text>
|
||||
<Pressable onPress={onClose} className={({ pressed }) => `p-1 ${pressed ? 'opacity-50' : ''}`}>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
className={({ pressed }) => `p-1 ${pressed ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<Text className="text-primary text-base">Done</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView horizontal className="border-b border-border" contentContainerClassName="px-3 py-2 gap-2">
|
||||
<ScrollView
|
||||
horizontal
|
||||
className="border-b border-border"
|
||||
contentContainerClassName="px-3 py-2 gap-2"
|
||||
>
|
||||
{reactions.map((r) => (
|
||||
<Pressable
|
||||
key={r.key}
|
||||
onPress={() => setSelectedKey(r.key)}
|
||||
className={`flex-row items-center gap-1 px-3 py-1.5 rounded-full border ${
|
||||
(selected?.key === r.key) ? 'bg-primary/20 border-primary/40' : 'bg-surface border-border'
|
||||
selected?.key === r.key
|
||||
? 'bg-primary/20 border-primary/40'
|
||||
: 'bg-surface border-border'
|
||||
}`}
|
||||
>
|
||||
<Text className="text-sm">{r.key}</Text>
|
||||
<Text className={`text-sm ${(selected?.key === r.key) ? 'text-primary' : 'text-muted-foreground'}`}>{r.count}</Text>
|
||||
<Text
|
||||
className={`text-sm ${selected?.key === r.key ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{r.count}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
|
@ -98,7 +147,9 @@ function ReactionDetailsModal({ reactions, visible, onClose }: { reactions: Mess
|
|||
{userId.replace(/^@/, '')[0]?.toUpperCase() ?? '?'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-foreground text-sm flex-1" numberOfLines={1}>{userId}</Text>
|
||||
<Text className="text-foreground text-sm flex-1" numberOfLines={1}>
|
||||
{userId}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
|
@ -107,7 +158,17 @@ function ReactionDetailsModal({ reactions, visible, onClose }: { reactions: Mess
|
|||
);
|
||||
}
|
||||
|
||||
export default function MessageBubble({ message, prevMessage, onReply, onEdit, onReact, onDelete, onForward, onImagePress, onAvatarPress }: Props) {
|
||||
export default function MessageBubble({
|
||||
message,
|
||||
prevMessage,
|
||||
onReply,
|
||||
onEdit,
|
||||
onReact,
|
||||
onDelete,
|
||||
onForward,
|
||||
onImagePress,
|
||||
onAvatarPress,
|
||||
}: Props) {
|
||||
const [showReactionDetails, setShowReactionDetails] = useState(false);
|
||||
const isOwn = message.isOwn;
|
||||
const isGrouped =
|
||||
|
|
@ -120,7 +181,14 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
|
||||
const handleLongPress = () => {
|
||||
const extraOptions = isOwn && !message.redacted ? ['Edit', 'Delete'] : [];
|
||||
const options = ['Cancel', 'Reply', 'Forward', ...QUICK_REACTIONS, 'Copy text', ...extraOptions];
|
||||
const options = [
|
||||
'Cancel',
|
||||
'Reply',
|
||||
'Forward',
|
||||
...QUICK_REACTIONS,
|
||||
'Copy text',
|
||||
...extraOptions,
|
||||
];
|
||||
const destructiveIndex = isOwn && !message.redacted ? options.length - 1 : undefined;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
|
|
@ -128,15 +196,32 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
{ options, cancelButtonIndex: 0, destructiveButtonIndex: destructiveIndex },
|
||||
(index) => {
|
||||
if (index === 0) return;
|
||||
if (index === 1) { onReply?.(message); return; }
|
||||
if (index === 2) { onForward?.(message); return; }
|
||||
if (index === 1) {
|
||||
onReply?.(message);
|
||||
return;
|
||||
}
|
||||
if (index === 2) {
|
||||
onForward?.(message);
|
||||
return;
|
||||
}
|
||||
const ri = index - 3;
|
||||
if (ri < QUICK_REACTIONS.length) { onReact?.(message.id, QUICK_REACTIONS[ri]); return; }
|
||||
if (ri < QUICK_REACTIONS.length) {
|
||||
onReact?.(message.id, QUICK_REACTIONS[ri]);
|
||||
return;
|
||||
}
|
||||
const ai = index - 3 - QUICK_REACTIONS.length;
|
||||
if (ai === 0) { Clipboard.setString(message.body); return; }
|
||||
if (ai === 1 && isOwn) { onEdit?.(message); return; }
|
||||
if (ai === 2 && isOwn) { onDelete?.(message.id); }
|
||||
},
|
||||
if (ai === 0) {
|
||||
Clipboard.setString(message.body);
|
||||
return;
|
||||
}
|
||||
if (ai === 1 && isOwn) {
|
||||
onEdit?.(message);
|
||||
return;
|
||||
}
|
||||
if (ai === 2 && isOwn) {
|
||||
onDelete?.(message.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Alert.alert('Message', undefined, [
|
||||
|
|
@ -145,7 +230,13 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
...(isOwn ? [{ text: 'Edit', onPress: () => onEdit?.(message) }] : []),
|
||||
{ text: 'Copy text', onPress: () => Clipboard.setString(message.body) },
|
||||
...(isOwn && !message.redacted
|
||||
? [{ text: 'Delete', style: 'destructive' as const, onPress: () => onDelete?.(message.id) }]
|
||||
? [
|
||||
{
|
||||
text: 'Delete',
|
||||
style: 'destructive' as const,
|
||||
onPress: () => onDelete?.(message.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ text: 'Cancel', style: 'cancel' as const },
|
||||
]);
|
||||
|
|
@ -170,10 +261,16 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
? (progress: Animated.SharedValue<number>) => <SwipeReplyAction progress={progress} />
|
||||
: undefined;
|
||||
|
||||
// Type assertion needed: react-native-gesture-handler Swipeable types expect old Animated API
|
||||
// but we use Reanimated v3 SharedValue which is what actually works at runtime
|
||||
const swipeableProps = {
|
||||
renderLeftActions: renderLeftActions as any,
|
||||
renderRightActions: renderRightActions as any,
|
||||
};
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
renderLeftActions={renderLeftActions}
|
||||
renderRightActions={renderRightActions}
|
||||
{...swipeableProps}
|
||||
onSwipeableOpen={(direction) => {
|
||||
if ((direction === 'left' && !isOwn) || (direction === 'right' && isOwn)) {
|
||||
onReply?.(message);
|
||||
|
|
@ -182,7 +279,9 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
friction={2}
|
||||
overshootFriction={8}
|
||||
>
|
||||
<View className={`flex-row items-end gap-2 ${isOwn ? 'justify-end' : 'justify-start'} ${isGrouped ? 'mb-0.5' : 'mb-3'} px-3`}>
|
||||
<View
|
||||
className={`flex-row items-end gap-2 ${isOwn ? 'justify-end' : 'justify-start'} ${isGrouped ? 'mb-0.5' : 'mb-3'} px-3`}
|
||||
>
|
||||
{/* Left avatar */}
|
||||
{!isOwn && (
|
||||
<View style={{ width: 28 }} className="mb-0.5">
|
||||
|
|
@ -200,7 +299,9 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
<View className={`max-w-[75%] ${isOwn ? 'items-end' : 'items-start'}`}>
|
||||
{showSenderName && (
|
||||
<Pressable onPress={onAvatarPress ? () => onAvatarPress(message.sender) : undefined}>
|
||||
<Text className="text-primary text-xs mb-1 ml-1 font-medium">{message.senderName}</Text>
|
||||
<Text className="text-primary text-xs mb-1 ml-1 font-medium">
|
||||
{message.senderName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
|
|
@ -218,10 +319,16 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
isOwn ? 'bg-white/10 border-white/40' : 'bg-primary/8 border-primary/40'
|
||||
}`}
|
||||
>
|
||||
<Text className={`text-xs font-semibold mb-0.5 ${isOwn ? 'text-white/80' : 'text-primary'}`} numberOfLines={1}>
|
||||
<Text
|
||||
className={`text-xs font-semibold mb-0.5 ${isOwn ? 'text-white/80' : 'text-primary'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{message.replyToSenderName ?? 'Unknown'}
|
||||
</Text>
|
||||
<Text className={`text-xs ${isOwn ? 'text-white/60' : 'text-muted-foreground'}`} numberOfLines={2}>
|
||||
<Text
|
||||
className={`text-xs ${isOwn ? 'text-white/60' : 'text-muted-foreground'}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{message.replyToBody ?? '…'}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -240,7 +347,10 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
{message.type === 'm.file' && (
|
||||
<View className="flex-row items-center gap-2 px-3 py-2">
|
||||
<Text className="text-2xl">📎</Text>
|
||||
<Text className={`text-sm flex-1 ${isOwn ? 'text-white' : 'text-foreground'}`} numberOfLines={1}>
|
||||
<Text
|
||||
className={`text-sm flex-1 ${isOwn ? 'text-white' : 'text-foreground'}`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{message.media?.filename ?? message.body}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -254,9 +364,15 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
/>
|
||||
)}
|
||||
|
||||
{(message.type === 'm.text' || message.type === 'm.notice' || message.type === 'm.emote') && (
|
||||
{(message.type === 'm.text' ||
|
||||
message.type === 'm.notice' ||
|
||||
message.type === 'm.emote') && (
|
||||
<MessageText
|
||||
body={message.type === 'm.emote' ? `* ${message.senderName} ${message.body}` : message.body}
|
||||
body={
|
||||
message.type === 'm.emote'
|
||||
? `* ${message.senderName} ${message.body}`
|
||||
: message.body
|
||||
}
|
||||
isOwn={isOwn}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -276,7 +392,9 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
>
|
||||
<Text className="text-xs">{r.key}</Text>
|
||||
{r.count > 1 && (
|
||||
<Text className={`text-xs ${r.includesMe ? 'text-primary' : 'text-muted-foreground'}`}>
|
||||
<Text
|
||||
className={`text-xs ${r.includesMe ? 'text-primary' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{r.count}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -305,7 +423,9 @@ export default function MessageBubble({ message, prevMessage, onReply, onEdit, o
|
|||
</View>
|
||||
))}
|
||||
{message.readBy.length > 3 && (
|
||||
<Text className="text-muted-foreground text-xs ml-0.5">+{message.readBy.length - 3}</Text>
|
||||
<Text className="text-muted-foreground text-xs ml-0.5">
|
||||
+{message.readBy.length - 3}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
// @ts-expect-error global polyfill
|
||||
// @ts-ignore global polyfill
|
||||
global.Buffer = Buffer;
|
||||
// @ts-expect-error global polyfill
|
||||
global.EventEmitter = EventEmitter;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { create } from 'zustand';
|
|||
import * as SecureStore from 'expo-secure-store';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import type { MatrixClient, Room, MatrixEvent } from 'matrix-js-sdk';
|
||||
import { NotificationCountType } from 'matrix-js-sdk/lib/models/room';
|
||||
import type {
|
||||
MatrixCredentials,
|
||||
SimpleRoom,
|
||||
|
|
@ -42,7 +43,13 @@ interface MatrixState {
|
|||
sendReaction: (eventId: string, key: string) => Promise<void>;
|
||||
redactMessage: (eventId: string) => Promise<void>;
|
||||
sendTyping: (typing: boolean) => Promise<void>;
|
||||
sendImage: (fileUri: string, filename: string, mimetype: string, width?: number, height?: number) => Promise<void>;
|
||||
sendImage: (
|
||||
fileUri: string,
|
||||
filename: string,
|
||||
mimetype: string,
|
||||
width?: number,
|
||||
height?: number
|
||||
) => Promise<void>;
|
||||
sendFile: (fileUri: string, filename: string, mimetype: string) => Promise<void>;
|
||||
editMessage: (eventId: string, newBody: string) => Promise<void>;
|
||||
sendVoice: (fileUri: string, durationMs: number) => Promise<void>;
|
||||
|
|
@ -64,7 +71,9 @@ function roomToSimple(room: Room, userId: string, baseUrl: string): SimpleRoom {
|
|||
})();
|
||||
|
||||
const rawAvatar = room.getMxcAvatarUrl?.() ?? null;
|
||||
const avatar = rawAvatar ? resolveMxcThumbnail(rawAvatar, baseUrl, 96, 96) ?? undefined : undefined;
|
||||
const avatar = rawAvatar
|
||||
? (resolveMxcThumbnail(rawAvatar, baseUrl, 96, 96) ?? undefined)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: room.roomId,
|
||||
|
|
@ -74,8 +83,8 @@ function roomToSimple(room: Room, userId: string, baseUrl: string): SimpleRoom {
|
|||
lastMessage: lastMsg?.getContent()?.body,
|
||||
lastMessageSender: lastMsg?.getSender() ?? undefined,
|
||||
lastMessageTime: room.getLastActiveTimestamp?.() ?? undefined,
|
||||
unreadCount: room.getUnreadNotificationCount('total') ?? 0,
|
||||
highlightCount: room.getUnreadNotificationCount('highlight') ?? 0,
|
||||
unreadCount: room.getUnreadNotificationCount(NotificationCountType.Total) ?? 0,
|
||||
highlightCount: room.getUnreadNotificationCount(NotificationCountType.Highlight) ?? 0,
|
||||
isDirect: !!dmUserId,
|
||||
isEncrypted: room.hasEncryptionStateEvent(),
|
||||
memberCount: room.getJoinedMemberCount(),
|
||||
|
|
@ -85,7 +94,12 @@ function roomToSimple(room: Room, userId: string, baseUrl: string): SimpleRoom {
|
|||
};
|
||||
}
|
||||
|
||||
function eventToMessage(event: MatrixEvent, userId: string, baseUrl: string, room?: Room): SimpleMessage | null {
|
||||
function eventToMessage(
|
||||
event: MatrixEvent,
|
||||
userId: string,
|
||||
baseUrl: string,
|
||||
room?: Room
|
||||
): SimpleMessage | null {
|
||||
if (event.getType() !== 'm.room.message') return null;
|
||||
|
||||
const content = event.getContent();
|
||||
|
|
@ -103,7 +117,9 @@ function eventToMessage(event: MatrixEvent, userId: string, baseUrl: string, roo
|
|||
width: content.info?.w,
|
||||
height: content.info?.h,
|
||||
filename: content.body,
|
||||
thumbnailUrl: isAudio ? undefined : (resolveMxcThumbnail(mxcUrl, baseUrl, 400, 300) ?? undefined),
|
||||
thumbnailUrl: isAudio
|
||||
? undefined
|
||||
: (resolveMxcThumbnail(mxcUrl, baseUrl, 400, 300) ?? undefined),
|
||||
downloadUrl: resolveMxcUrl(mxcUrl, baseUrl) ?? undefined,
|
||||
duration: content.info?.duration,
|
||||
};
|
||||
|
|
@ -113,7 +129,7 @@ function eventToMessage(event: MatrixEvent, userId: string, baseUrl: string, roo
|
|||
const senderMember = event.sender;
|
||||
const rawSenderAvatar = senderMember?.getMxcAvatarUrl?.() ?? null;
|
||||
const senderAvatar = rawSenderAvatar
|
||||
? resolveMxcThumbnail(rawSenderAvatar, baseUrl, 64, 64) ?? undefined
|
||||
? (resolveMxcThumbnail(rawSenderAvatar, baseUrl, 64, 64) ?? undefined)
|
||||
: undefined;
|
||||
|
||||
// Reply-to
|
||||
|
|
@ -133,16 +149,19 @@ function eventToMessage(event: MatrixEvent, userId: string, baseUrl: string, roo
|
|||
let reactions: MessageReaction[] | undefined;
|
||||
if (room) {
|
||||
const eventId = event.getId();
|
||||
const reactionEvents = room.getLiveTimeline().getEvents().filter(
|
||||
(e) =>
|
||||
e.getType() === 'm.reaction' &&
|
||||
e.getContent()?.['m.relates_to']?.event_id === eventId &&
|
||||
e.getContent()?.['m.relates_to']?.rel_type === 'm.annotation',
|
||||
);
|
||||
const reactionEvents = room
|
||||
.getLiveTimeline()
|
||||
.getEvents()
|
||||
.filter(
|
||||
(e) =>
|
||||
e.getType() === 'm.reaction' &&
|
||||
e.getContent()?.['m.relates_to']?.event_id === eventId &&
|
||||
e.getContent()?.['m.relates_to']?.rel_type === 'm.annotation'
|
||||
);
|
||||
if (reactionEvents.length > 0) {
|
||||
const grouped = new Map<string, { users: string[]; includesMe: boolean }>();
|
||||
for (const re of reactionEvents) {
|
||||
const key = re.getContent()['m.relates_to'].key as string;
|
||||
const key = re.getContent()?.['m.relates_to']?.key as string;
|
||||
if (!grouped.has(key)) grouped.set(key, { users: [], includesMe: false });
|
||||
const entry = grouped.get(key)!;
|
||||
const sender = re.getSender() ?? '';
|
||||
|
|
@ -261,7 +280,9 @@ export const useMatrixStore = create<MatrixState>((set, get) => ({
|
|||
try {
|
||||
const cached = await AsyncStorage.getItem(ROOMS_CACHE_KEY);
|
||||
if (cached) set({ rooms: JSON.parse(cached) });
|
||||
} catch { /* ignore cache errors */ }
|
||||
} catch {
|
||||
/* ignore cache errors */
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
const rooms = buildSimpleRooms(client, userId, baseUrl);
|
||||
|
|
@ -347,7 +368,13 @@ export const useMatrixStore = create<MatrixState>((set, get) => ({
|
|||
|
||||
selectRoom: (roomId: string) => {
|
||||
const { client, credentials } = get();
|
||||
set({ currentRoomId: roomId, typingUsers: [], messages: [], roomMembers: [], firstUnreadEventId: null });
|
||||
set({
|
||||
currentRoomId: roomId,
|
||||
typingUsers: [],
|
||||
messages: [],
|
||||
roomMembers: [],
|
||||
firstUnreadEventId: null,
|
||||
});
|
||||
if (!client || !credentials) return;
|
||||
|
||||
const room = client.getRoom(roomId);
|
||||
|
|
@ -356,14 +383,16 @@ export const useMatrixStore = create<MatrixState>((set, get) => ({
|
|||
// Capture first unread event before marking as read
|
||||
const { userId, homeserver: baseUrl } = credentials;
|
||||
let firstUnreadEventId: string | null = null;
|
||||
const unreadCount = room.getUnreadNotificationCount('total') ?? 0;
|
||||
const unreadCount = room.getUnreadNotificationCount(NotificationCountType.Total) ?? 0;
|
||||
if (unreadCount > 0) {
|
||||
const lastReadEventId = (room as any).getEventReadUpTo?.(userId) as string | null;
|
||||
if (lastReadEventId) {
|
||||
const timeline = room.getLiveTimeline().getEvents();
|
||||
const lastReadIdx = timeline.findIndex((e) => e.getId() === lastReadEventId);
|
||||
if (lastReadIdx >= 0) {
|
||||
const firstUnread = timeline.slice(lastReadIdx + 1).find((e) => e.getType() === 'm.room.message');
|
||||
const firstUnread = timeline
|
||||
.slice(lastReadIdx + 1)
|
||||
.find((e) => e.getType() === 'm.room.message');
|
||||
firstUnreadEventId = firstUnread?.getId() ?? null;
|
||||
}
|
||||
}
|
||||
|
|
@ -391,7 +420,7 @@ export const useMatrixStore = create<MatrixState>((set, get) => ({
|
|||
userId: m.userId,
|
||||
displayName: m.name || m.userId,
|
||||
avatarUrl: rawAvatar
|
||||
? resolveMxcThumbnail(rawAvatar, credentials.homeserver, 64, 64) ?? undefined
|
||||
? (resolveMxcThumbnail(rawAvatar, credentials.homeserver, 64, 64) ?? undefined)
|
||||
: undefined,
|
||||
membership: 'join' as const,
|
||||
powerLevel: m.powerLevel ?? 0,
|
||||
|
|
|
|||
|
|
@ -116,8 +116,6 @@
|
|||
"photos:db:push": "pnpm --filter @photos/backend db:push",
|
||||
"photos:db:studio": "pnpm --filter @photos/backend db:studio",
|
||||
"dev:tags-test": "./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh calendar && ./scripts/setup-databases.sh contacts && ./scripts/setup-databases.sh auth && concurrently -n auth,todo-be,todo-web,cal-be,cal-web,con-be,con-web -c blue,green,cyan,yellow,magenta,red,white \"pnpm dev:auth\" \"pnpm dev:todo:backend\" \"pnpm dev:todo:web\" \"pnpm dev:calendar:backend\" \"pnpm dev:calendar:web\" \"pnpm dev:contacts:backend\" \"pnpm dev:contacts:web\"",
|
||||
"matrix:dev": "turbo run dev --filter=matrix...",
|
||||
"dev:matrix:web": "pnpm --filter @matrix/web dev",
|
||||
"moodlit:dev": "turbo run dev --filter=moodlit...",
|
||||
"dev:moodlit:mobile": "pnpm --filter @moodlit/mobile dev",
|
||||
"dev:moodlit:web": "pnpm --filter @moodlit/web dev",
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@
|
|||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "vitest.config.ts"],
|
||||
"exclude": ["node_modules", "src/**/*.svelte"]
|
||||
"exclude": ["node_modules", "src/**/*.svelte", "src/svelte"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,5 @@
|
|||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "src/svelte"]
|
||||
}
|
||||
|
|
|
|||
2886
pnpm-lock.yaml
generated
2886
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue