@mereb/app-messaging 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/native.js +35 -0
- package/dist/store.d.ts +26 -0
- package/dist/store.js +77 -0
- package/package.json +20 -3
- package/src/index.tsx +1 -9
- package/src/native.tsx +47 -0
- package/src/store.ts +111 -0
- /package/dist/{src/index.d.ts → native.d.ts} +0 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './store';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './store';
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { Text, View } from 'react-native';
|
|
3
|
+
import { createInMemoryMessagingStore } from './store';
|
|
4
|
+
export function MessagesScreen() {
|
|
5
|
+
const store = useMemo(() => createInMemoryMessagingStore(), []);
|
|
6
|
+
const [version, setVersion] = useState(0);
|
|
7
|
+
const conversations = store.getConversations();
|
|
8
|
+
return (<View style={{ flex: 1, padding: 24, gap: 16 }}>
|
|
9
|
+
<Text style={{ fontWeight: '700', fontSize: 18, marginBottom: 8 }}>Messaging</Text>
|
|
10
|
+
{conversations.map((conversation) => (<View key={conversation.id} style={{
|
|
11
|
+
padding: 12,
|
|
12
|
+
borderRadius: 12,
|
|
13
|
+
borderWidth: 1,
|
|
14
|
+
borderColor: '#E2E8F0',
|
|
15
|
+
backgroundColor: '#FFFFFF',
|
|
16
|
+
gap: 4
|
|
17
|
+
}}>
|
|
18
|
+
<Text style={{ fontWeight: '600', fontSize: 16 }}>{conversation.title}</Text>
|
|
19
|
+
<Text style={{ color: '#475569' }}>{conversation.lastMessage?.body ?? 'No messages yet'}</Text>
|
|
20
|
+
<Text style={{ color: '#94A3B8', fontSize: 12 }}>Unread: {conversation.unreadCount}</Text>
|
|
21
|
+
</View>))}
|
|
22
|
+
<Text onPress={() => {
|
|
23
|
+
store.sendMessage(conversations[0]?.id ?? 'conv-1', 'Sent from mobile preview', {
|
|
24
|
+
id: 'mobile-user',
|
|
25
|
+
name: 'You'
|
|
26
|
+
});
|
|
27
|
+
setVersion((v) => v + 1);
|
|
28
|
+
}} style={{ color: '#2563EB', fontWeight: '600' }}>
|
|
29
|
+
Send a quick test message
|
|
30
|
+
</Text>
|
|
31
|
+
<Text style={{ color: '#94A3B8', fontSize: 12 }}>
|
|
32
|
+
Version {version} • This view uses the shared in-memory store so it stays in sync with the web shell preview.
|
|
33
|
+
</Text>
|
|
34
|
+
</View>);
|
|
35
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type MessagingMessage = {
|
|
2
|
+
id: string;
|
|
3
|
+
conversationId: string;
|
|
4
|
+
senderId: string;
|
|
5
|
+
senderName?: string;
|
|
6
|
+
body: string;
|
|
7
|
+
sentAt: string;
|
|
8
|
+
};
|
|
9
|
+
export type MessagingConversation = {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
participantIds: string[];
|
|
13
|
+
lastMessage?: MessagingMessage;
|
|
14
|
+
unreadCount: number;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
};
|
|
17
|
+
export type MessagingStore = {
|
|
18
|
+
getConversations: () => MessagingConversation[];
|
|
19
|
+
getConversation: (id: string) => MessagingConversation | undefined;
|
|
20
|
+
getMessages: (conversationId: string) => MessagingMessage[];
|
|
21
|
+
sendMessage: (conversationId: string, body: string, sender?: {
|
|
22
|
+
id: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
}) => MessagingMessage;
|
|
25
|
+
};
|
|
26
|
+
export declare function createInMemoryMessagingStore(seed?: MessagingConversation[]): MessagingStore;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const seedConversations = [
|
|
2
|
+
{
|
|
3
|
+
id: 'conv-1',
|
|
4
|
+
title: 'Platform Ops',
|
|
5
|
+
participantIds: ['u-platform', 'u-release', 'u-ops'],
|
|
6
|
+
unreadCount: 2,
|
|
7
|
+
updatedAt: new Date().toISOString(),
|
|
8
|
+
lastMessage: {
|
|
9
|
+
id: 'msg-1',
|
|
10
|
+
conversationId: 'conv-1',
|
|
11
|
+
senderId: 'u-release',
|
|
12
|
+
senderName: 'Release Manager',
|
|
13
|
+
body: 'Copy that. Will queue prod promotion after smoke.',
|
|
14
|
+
sentAt: new Date().toISOString()
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'conv-2',
|
|
19
|
+
title: 'Support triage',
|
|
20
|
+
participantIds: ['u-support', 'u-oncall', 'u-platform'],
|
|
21
|
+
unreadCount: 0,
|
|
22
|
+
updatedAt: new Date().toISOString(),
|
|
23
|
+
lastMessage: {
|
|
24
|
+
id: 'msg-2',
|
|
25
|
+
conversationId: 'conv-2',
|
|
26
|
+
senderId: 'u-oncall',
|
|
27
|
+
senderName: 'On-call',
|
|
28
|
+
body: 'Acknowledged. Looking at the router dashboards now.',
|
|
29
|
+
sentAt: new Date().toISOString()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
export function createInMemoryMessagingStore(seed = seedConversations) {
|
|
34
|
+
let conversations = seed.map((conversation) => ({
|
|
35
|
+
...conversation,
|
|
36
|
+
lastMessage: conversation.lastMessage
|
|
37
|
+
? { ...conversation.lastMessage, sentAt: conversation.lastMessage.sentAt ?? new Date().toISOString() }
|
|
38
|
+
: undefined
|
|
39
|
+
}));
|
|
40
|
+
const messageLookup = {};
|
|
41
|
+
conversations.forEach((conversation) => {
|
|
42
|
+
messageLookup[conversation.id] = conversation.lastMessage ? [conversation.lastMessage] : [];
|
|
43
|
+
});
|
|
44
|
+
const refreshConversation = (conversationId, message) => {
|
|
45
|
+
const idx = conversations.findIndex((c) => c.id === conversationId);
|
|
46
|
+
if (idx >= 0) {
|
|
47
|
+
conversations[idx] = {
|
|
48
|
+
...conversations[idx],
|
|
49
|
+
lastMessage: message,
|
|
50
|
+
updatedAt: message.sentAt,
|
|
51
|
+
unreadCount: Math.max(0, conversations[idx].unreadCount - 1)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
getConversations: () => [...conversations].sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)),
|
|
57
|
+
getConversation: (id) => conversations.find((conversation) => conversation.id === id),
|
|
58
|
+
getMessages: (conversationId) => [...(messageLookup[conversationId] ?? [])],
|
|
59
|
+
sendMessage: (conversationId, body, sender) => {
|
|
60
|
+
const conversationExists = conversations.some((conversation) => conversation.id === conversationId);
|
|
61
|
+
if (!conversationExists) {
|
|
62
|
+
throw new Error('Conversation not found');
|
|
63
|
+
}
|
|
64
|
+
const message = {
|
|
65
|
+
id: `msg-${Math.random().toString(36).slice(2, 8)}`,
|
|
66
|
+
conversationId,
|
|
67
|
+
senderId: sender?.id ?? 'anonymous',
|
|
68
|
+
senderName: sender?.name ?? 'You',
|
|
69
|
+
body,
|
|
70
|
+
sentAt: new Date().toISOString()
|
|
71
|
+
};
|
|
72
|
+
messageLookup[conversationId] = [...(messageLookup[conversationId] ?? []), message];
|
|
73
|
+
refreshConversation(conversationId, message);
|
|
74
|
+
return message;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mereb/app-messaging",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Messaging experience primitives for Mereb applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./store": {
|
|
15
|
+
"types": "./dist/store.d.ts",
|
|
16
|
+
"import": "./dist/store.js",
|
|
17
|
+
"default": "./dist/store.js"
|
|
18
|
+
},
|
|
19
|
+
"./native": {
|
|
20
|
+
"types": "./dist/native.d.ts",
|
|
21
|
+
"import": "./dist/native.js",
|
|
22
|
+
"default": "./dist/native.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
8
25
|
"files": [
|
|
9
26
|
"src",
|
|
10
27
|
"dist",
|
package/src/index.tsx
CHANGED
package/src/native.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
2
|
+
import { Text, View } from 'react-native'
|
|
3
|
+
import { createInMemoryMessagingStore } from './store'
|
|
4
|
+
|
|
5
|
+
export function MessagesScreen() {
|
|
6
|
+
const store = useMemo(() => createInMemoryMessagingStore(), [])
|
|
7
|
+
const [version, setVersion] = useState(0)
|
|
8
|
+
const conversations = store.getConversations()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<View style={{ flex: 1, padding: 24, gap: 16 }}>
|
|
12
|
+
<Text style={{ fontWeight: '700', fontSize: 18, marginBottom: 8 }}>Messaging</Text>
|
|
13
|
+
{conversations.map((conversation) => (
|
|
14
|
+
<View
|
|
15
|
+
key={conversation.id}
|
|
16
|
+
style={{
|
|
17
|
+
padding: 12,
|
|
18
|
+
borderRadius: 12,
|
|
19
|
+
borderWidth: 1,
|
|
20
|
+
borderColor: '#E2E8F0',
|
|
21
|
+
backgroundColor: '#FFFFFF',
|
|
22
|
+
gap: 4
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<Text style={{ fontWeight: '600', fontSize: 16 }}>{conversation.title}</Text>
|
|
26
|
+
<Text style={{ color: '#475569' }}>{conversation.lastMessage?.body ?? 'No messages yet'}</Text>
|
|
27
|
+
<Text style={{ color: '#94A3B8', fontSize: 12 }}>Unread: {conversation.unreadCount}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
))}
|
|
30
|
+
<Text
|
|
31
|
+
onPress={() => {
|
|
32
|
+
store.sendMessage(conversations[0]?.id ?? 'conv-1', 'Sent from mobile preview', {
|
|
33
|
+
id: 'mobile-user',
|
|
34
|
+
name: 'You'
|
|
35
|
+
})
|
|
36
|
+
setVersion((v) => v + 1)
|
|
37
|
+
}}
|
|
38
|
+
style={{ color: '#2563EB', fontWeight: '600' }}
|
|
39
|
+
>
|
|
40
|
+
Send a quick test message
|
|
41
|
+
</Text>
|
|
42
|
+
<Text style={{ color: '#94A3B8', fontSize: 12 }}>
|
|
43
|
+
Version {version} • This view uses the shared in-memory store so it stays in sync with the web shell preview.
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
)
|
|
47
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export type MessagingMessage = {
|
|
2
|
+
id: string
|
|
3
|
+
conversationId: string
|
|
4
|
+
senderId: string
|
|
5
|
+
senderName?: string
|
|
6
|
+
body: string
|
|
7
|
+
sentAt: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MessagingConversation = {
|
|
11
|
+
id: string
|
|
12
|
+
title: string
|
|
13
|
+
participantIds: string[]
|
|
14
|
+
lastMessage?: MessagingMessage
|
|
15
|
+
unreadCount: number
|
|
16
|
+
updatedAt: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type MessagingStore = {
|
|
20
|
+
getConversations: () => MessagingConversation[]
|
|
21
|
+
getConversation: (id: string) => MessagingConversation | undefined
|
|
22
|
+
getMessages: (conversationId: string) => MessagingMessage[]
|
|
23
|
+
sendMessage: (
|
|
24
|
+
conversationId: string,
|
|
25
|
+
body: string,
|
|
26
|
+
sender?: { id: string; name?: string }
|
|
27
|
+
) => MessagingMessage
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const seedConversations: MessagingConversation[] = [
|
|
31
|
+
{
|
|
32
|
+
id: 'conv-1',
|
|
33
|
+
title: 'Platform Ops',
|
|
34
|
+
participantIds: ['u-platform', 'u-release', 'u-ops'],
|
|
35
|
+
unreadCount: 2,
|
|
36
|
+
updatedAt: new Date().toISOString(),
|
|
37
|
+
lastMessage: {
|
|
38
|
+
id: 'msg-1',
|
|
39
|
+
conversationId: 'conv-1',
|
|
40
|
+
senderId: 'u-release',
|
|
41
|
+
senderName: 'Release Manager',
|
|
42
|
+
body: 'Copy that. Will queue prod promotion after smoke.',
|
|
43
|
+
sentAt: new Date().toISOString()
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'conv-2',
|
|
48
|
+
title: 'Support triage',
|
|
49
|
+
participantIds: ['u-support', 'u-oncall', 'u-platform'],
|
|
50
|
+
unreadCount: 0,
|
|
51
|
+
updatedAt: new Date().toISOString(),
|
|
52
|
+
lastMessage: {
|
|
53
|
+
id: 'msg-2',
|
|
54
|
+
conversationId: 'conv-2',
|
|
55
|
+
senderId: 'u-oncall',
|
|
56
|
+
senderName: 'On-call',
|
|
57
|
+
body: 'Acknowledged. Looking at the router dashboards now.',
|
|
58
|
+
sentAt: new Date().toISOString()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
export function createInMemoryMessagingStore(seed: MessagingConversation[] = seedConversations): MessagingStore {
|
|
64
|
+
let conversations: MessagingConversation[] = seed.map((conversation) => ({
|
|
65
|
+
...conversation,
|
|
66
|
+
lastMessage: conversation.lastMessage
|
|
67
|
+
? { ...conversation.lastMessage, sentAt: conversation.lastMessage.sentAt ?? new Date().toISOString() }
|
|
68
|
+
: undefined
|
|
69
|
+
}))
|
|
70
|
+
const messageLookup: Record<string, MessagingMessage[]> = {}
|
|
71
|
+
|
|
72
|
+
conversations.forEach((conversation) => {
|
|
73
|
+
messageLookup[conversation.id] = conversation.lastMessage ? [conversation.lastMessage] : []
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const refreshConversation = (conversationId: string, message: MessagingMessage) => {
|
|
77
|
+
const idx = conversations.findIndex((c) => c.id === conversationId)
|
|
78
|
+
if (idx >= 0) {
|
|
79
|
+
conversations[idx] = {
|
|
80
|
+
...conversations[idx],
|
|
81
|
+
lastMessage: message,
|
|
82
|
+
updatedAt: message.sentAt,
|
|
83
|
+
unreadCount: Math.max(0, conversations[idx].unreadCount - 1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
getConversations: () => [...conversations].sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)),
|
|
90
|
+
getConversation: (id: string) => conversations.find((conversation) => conversation.id === id),
|
|
91
|
+
getMessages: (conversationId: string) => [...(messageLookup[conversationId] ?? [])],
|
|
92
|
+
sendMessage: (conversationId: string, body: string, sender?: { id: string; name?: string }) => {
|
|
93
|
+
const conversationExists = conversations.some((conversation) => conversation.id === conversationId)
|
|
94
|
+
if (!conversationExists) {
|
|
95
|
+
throw new Error('Conversation not found')
|
|
96
|
+
}
|
|
97
|
+
const message: MessagingMessage = {
|
|
98
|
+
id: `msg-${Math.random().toString(36).slice(2, 8)}`,
|
|
99
|
+
conversationId,
|
|
100
|
+
senderId: sender?.id ?? 'anonymous',
|
|
101
|
+
senderName: sender?.name ?? 'You',
|
|
102
|
+
body,
|
|
103
|
+
sentAt: new Date().toISOString()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
messageLookup[conversationId] = [...(messageLookup[conversationId] ?? []), message]
|
|
107
|
+
refreshConversation(conversationId, message)
|
|
108
|
+
return message
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
File without changes
|