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:
Till JS 2026-03-06 19:36:39 +01:00
parent a9c05ca46b
commit bc3a527bf4
16 changed files with 2555 additions and 694 deletions

View file

@ -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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View file

@ -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": {

View file

@ -1 +1,10 @@
/// <reference types="nativewind/types" />
import 'react-native';
declare module 'react-native' {
interface PressableProps {
className?: string | ((state: { pressed: boolean }) => string);
cssInterop?: boolean;
}
}

View file

@ -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",

View file

@ -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>
)}

View file

@ -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;

View file

@ -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,

View file

@ -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",

View file

@ -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"]
}

View file

@ -15,5 +15,5 @@
"noEmit": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "src/svelte"]
}

2886
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff