frontend client for gemstone. decentralised workplace app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: initial implementation

serenity 494b24c1 c431f745

+224
+124
app/components/ChatComponent.tsx
··· 1 + import { useWebSocket } from "@/app/hooks/useWebSocket"; 2 + import { useState } from "react"; 3 + import { 4 + View, 5 + Text, 6 + TextInput, 7 + TouchableOpacity, 8 + StyleSheet, 9 + ScrollView, 10 + } from "react-native"; 11 + 12 + export default function ChatComponent() { 13 + const [inputText, setInputText] = useState(""); 14 + const { messages, isConnected, sendMessage } = useWebSocket( 15 + "ws://localhost:8080", 16 + ); 17 + 18 + const handleSend = () => { 19 + if (inputText.trim()) { 20 + sendMessage(inputText); 21 + setInputText(""); 22 + } 23 + }; 24 + 25 + return ( 26 + <View style={styles.container}> 27 + <View style={styles.header}> 28 + <Text style={styles.status}> 29 + {isConnected ? "🟢 Connected" : "🔴 Disconnected"} 30 + </Text> 31 + </View> 32 + 33 + <ScrollView style={styles.messagesContainer}> 34 + {messages.map((msg, index) => ( 35 + <View key={index} style={styles.messageItem}> 36 + <Text style={styles.messageText}>{msg.text}</Text> 37 + <Text style={styles.timestamp}> 38 + {new Date(msg.timestamp).toLocaleTimeString()} 39 + </Text> 40 + </View> 41 + ))} 42 + </ScrollView> 43 + 44 + <View style={styles.inputContainer}> 45 + <TextInput 46 + style={styles.input} 47 + value={inputText} 48 + onChangeText={setInputText} 49 + placeholder="Type a message..." 50 + onSubmitEditing={handleSend} 51 + /> 52 + <TouchableOpacity 53 + style={styles.button} 54 + onPress={handleSend} 55 + disabled={!isConnected} 56 + > 57 + <Text style={styles.buttonText}>Send</Text> 58 + </TouchableOpacity> 59 + </View> 60 + </View> 61 + ); 62 + } 63 + 64 + const styles = StyleSheet.create({ 65 + container: { 66 + flex: 1, 67 + backgroundColor: "#fff", 68 + }, 69 + header: { 70 + padding: 16, 71 + borderBottomWidth: 1, 72 + borderBottomColor: "#e0e0e0", 73 + }, 74 + status: { 75 + fontSize: 14, 76 + fontWeight: "600", 77 + }, 78 + messagesContainer: { 79 + flex: 1, 80 + padding: 16, 81 + }, 82 + messageItem: { 83 + marginBottom: 12, 84 + padding: 12, 85 + backgroundColor: "#f5f5f5", 86 + borderRadius: 8, 87 + }, 88 + messageText: { 89 + fontSize: 16, 90 + marginBottom: 4, 91 + }, 92 + timestamp: { 93 + fontSize: 12, 94 + color: "#666", 95 + }, 96 + inputContainer: { 97 + flexDirection: "row", 98 + padding: 16, 99 + borderTopWidth: 1, 100 + borderTopColor: "#e0e0e0", 101 + }, 102 + input: { 103 + flex: 1, 104 + borderWidth: 1, 105 + borderColor: "#ccc", 106 + borderRadius: 8, 107 + paddingHorizontal: 12, 108 + paddingVertical: 8, 109 + marginRight: 8, 110 + fontSize: 16, 111 + }, 112 + button: { 113 + backgroundColor: "#007AFF", 114 + paddingHorizontal: 20, 115 + paddingVertical: 10, 116 + borderRadius: 8, 117 + justifyContent: "center", 118 + }, 119 + buttonText: { 120 + color: "#fff", 121 + fontSize: 16, 122 + fontWeight: "600", 123 + }, 124 + });
+60
app/hooks/useWebSocket.ts
··· 1 + import { useEffect, useRef, useState } from "react"; 2 + 3 + export function useWebSocket(url: string) { 4 + const [messages, setMessages] = useState< 5 + { text: string; timestamp: string }[] 6 + >([]); 7 + const [isConnected, setIsConnected] = useState(false); 8 + const ws = useRef<WebSocket | null>(null); 9 + 10 + useEffect(() => { 11 + // Connect to WebSocket 12 + ws.current = new WebSocket(url); 13 + 14 + ws.current.onopen = () => { 15 + console.log("Connected to WebSocket"); 16 + setIsConnected(true); 17 + }; 18 + 19 + ws.current.onmessage = (event) => { 20 + const data = JSON.parse(event.data); 21 + 22 + if (data.type === "shard/history") { 23 + setMessages(data.messages); 24 + } else if (data.type === "shard/message") { 25 + setMessages((prev) => [ 26 + ...prev, 27 + { text: data.text, timestamp: data.timestamp }, 28 + ]); 29 + } 30 + }; 31 + 32 + ws.current.onerror = (error) => { 33 + console.error("WebSocket error:", error); 34 + }; 35 + 36 + ws.current.onclose = () => { 37 + console.log("Disconnected from WebSocket"); 38 + setIsConnected(false); 39 + }; 40 + 41 + // Cleanup on unmount 42 + return () => { 43 + ws.current?.close(); 44 + }; 45 + }, [url]); 46 + 47 + const sendMessage = (text: string) => { 48 + if (ws.current?.readyState === WebSocket.OPEN) { 49 + ws.current.send( 50 + JSON.stringify({ 51 + type: "shard/message", 52 + text, 53 + timestamp: new Date(), 54 + }), 55 + ); 56 + } 57 + }; 58 + 59 + return { messages, isConnected, sendMessage }; 60 + }
+1
app/index.tsx
··· 11 11 }} 12 12 > 13 13 <Text>Edit app/index.tsx to edit this screen.</Text> 14 + <ChatComponent /> 14 15 </View> 15 16 ); 16 17 }
+39
app/lib/validator.ts
··· 1 + export function assertShardMessage( 2 + json: unknown, 3 + ): asserts json is ShardMessage { 4 + if (typeof json !== "object") { 5 + throw new Error("not a js object"); 6 + } 7 + 8 + const candidate = json as Record<string, unknown>; 9 + 10 + if (candidate.type !== "shard/message") { 11 + throw new Error("Invalid type"); 12 + } 13 + 14 + if (typeof candidate.text !== "string") { 15 + throw new Error("Invalid text"); 16 + } 17 + 18 + const timestamp = new Date(candidate.timestamp as string); 19 + 20 + if (!(timestamp instanceof Date)) { 21 + throw new Error("Invalid timestamp"); 22 + } 23 + } 24 + 25 + // example. we will use zod in the future. 26 + export const validateShardMessage = (json: unknown) => { 27 + try { 28 + assertShardMessage(json); 29 + return { success: true, data: json }; 30 + } catch (e: unknown) { 31 + return { error: e }; 32 + } 33 + }; 34 + 35 + export interface ShardMessage { 36 + type: "shard/message"; 37 + text: string; 38 + timestamp: Date; 39 + }