@mereb/app-messaging 0.0.2 → 0.0.4

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/gql.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const CONVERSATIONS: import("@apollo/client").DocumentNode;
2
+ export declare const MESSAGES: import("@apollo/client").DocumentNode;
3
+ export declare const SEND_MESSAGE: import("@apollo/client").DocumentNode;
package/dist/gql.js ADDED
@@ -0,0 +1,50 @@
1
+ import { gql } from '@apollo/client';
2
+ export const CONVERSATIONS = gql `
3
+ query Conversations {
4
+ conversations {
5
+ id
6
+ title
7
+ participantIds
8
+ unreadCount
9
+ updatedAt
10
+ lastMessage {
11
+ id
12
+ senderName
13
+ body
14
+ sentAt
15
+ conversationId
16
+ }
17
+ }
18
+ }
19
+ `;
20
+ export const MESSAGES = gql `
21
+ query ConversationMessages($conversationId: ID!, $after: String) {
22
+ messages(conversationId: $conversationId, after: $after, limit: 50) {
23
+ edges {
24
+ cursor
25
+ node {
26
+ id
27
+ conversationId
28
+ senderName
29
+ body
30
+ sentAt
31
+ }
32
+ }
33
+ pageInfo {
34
+ endCursor
35
+ hasNextPage
36
+ }
37
+ }
38
+ }
39
+ `;
40
+ export const SEND_MESSAGE = gql `
41
+ mutation SendMessage($conversationId: ID, $toUserId: ID, $body: String!) {
42
+ sendMessage(conversationId: $conversationId, toUserId: $toUserId, body: $body) {
43
+ id
44
+ conversationId
45
+ senderName
46
+ body
47
+ sentAt
48
+ }
49
+ }
50
+ `;
@@ -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
+ }
@@ -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,32 @@
1
1
  {
2
2
  "name": "@mereb/app-messaging",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Messaging experience primitives for Mereb applications",
5
5
  "type": "module",
6
- "main": "src/index.tsx",
7
- "types": "src/index.tsx",
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
+ "./gql": {
25
+ "types": "./dist/gql.d.ts",
26
+ "import": "./dist/gql.js",
27
+ "default": "./dist/gql.js"
28
+ }
29
+ },
8
30
  "files": [
9
31
  "src",
10
32
  "dist",
@@ -13,10 +35,14 @@
13
35
  "peerDependencies": {
14
36
  "expo-router": ">=3.0.0",
15
37
  "react": ">=18.2.0",
16
- "react-native": ">=0.72.0"
38
+ "react-native": ">=0.72.0",
39
+ "@apollo/client": ">=3.0.0",
40
+ "graphql": ">=16.0.0"
17
41
  },
18
42
  "devDependencies": {
43
+ "@apollo/client": "^3.12.5",
19
44
  "@types/react": "~18.2.79",
45
+ "graphql": "^16.11.0",
20
46
  "typescript": ">=5.3.3"
21
47
  },
22
48
  "scripts": {
package/src/gql.ts ADDED
@@ -0,0 +1,53 @@
1
+ import {gql} from '@apollo/client'
2
+
3
+ export const CONVERSATIONS = gql`
4
+ query Conversations {
5
+ conversations {
6
+ id
7
+ title
8
+ participantIds
9
+ unreadCount
10
+ updatedAt
11
+ lastMessage {
12
+ id
13
+ senderName
14
+ body
15
+ sentAt
16
+ conversationId
17
+ }
18
+ }
19
+ }
20
+ `
21
+
22
+ export const MESSAGES = gql`
23
+ query ConversationMessages($conversationId: ID!, $after: String) {
24
+ messages(conversationId: $conversationId, after: $after, limit: 50) {
25
+ edges {
26
+ cursor
27
+ node {
28
+ id
29
+ conversationId
30
+ senderName
31
+ body
32
+ sentAt
33
+ }
34
+ }
35
+ pageInfo {
36
+ endCursor
37
+ hasNextPage
38
+ }
39
+ }
40
+ }
41
+ `
42
+
43
+ export const SEND_MESSAGE = gql`
44
+ mutation SendMessage($conversationId: ID, $toUserId: ID, $body: String!) {
45
+ sendMessage(conversationId: $conversationId, toUserId: $toUserId, body: $body) {
46
+ id
47
+ conversationId
48
+ senderName
49
+ body
50
+ sentAt
51
+ }
52
+ }
53
+ `
package/src/index.tsx CHANGED
@@ -1,9 +1 @@
1
- import { Text, View } from 'react-native';
2
-
3
- export function MessagesScreen() {
4
- return (
5
- <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
6
- <Text>Messages coming soon</Text>
7
- </View>
8
- );
9
- }
1
+ export * from './store'
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