Skip to content

Commit

Permalink
[arcade-chat] Update channel metadata via Nostr kind 41 (#30)
Browse files Browse the repository at this point in the history
* Add channelmetadata NostrKind and sub to it

* Initial useChannelMetadata

* useChannelMetadata uses creation info if no updated metadata

* ChannelScreen sets title based on metadata name

* Pass channelMetadata to ChannelAvatar

* initial updateDemoChannelMetadata

* create updateDemoChannelMetadata

* Sending update messge - just looking for tag

* Cleanup

* Set channelId to #e tag and fix tags typedef

* Extract and fix isArrayInArray

* Setting+filtering events for messages+metadata

* Use most recent channel metadata event

* shhh

* clear textinput after submit

* Add useDebounce

* useChannelMessages w useDebounce

* debounce useChannelMetadata

* cleanup

* Reconnect WS on close

* better debounce for useChannelMetadata

* Updating name+pic in metadata via modal
  • Loading branch information
AtlantisPleb authored Aug 3, 2022
1 parent 75f8bfa commit a33b06d
Show file tree
Hide file tree
Showing 24 changed files with 397 additions and 100 deletions.
7 changes: 4 additions & 3 deletions apps/arcade-chat/components/ChannelAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Channel } from '@arcadecity/use-arcade'
import { Channel, ChannelMetadata } from '@arcadecity/use-arcade'
import { Image } from 'react-native'

export const ChannelAvatar = ({ channel }: { channel: Channel }) => {
export const ChannelAvatar = ({ metadata }: { metadata: ChannelMetadata }) => {
// console.log(metadata.picture)
return (
<Image
source={{ uri: channel.image }}
source={{ uri: metadata?.picture ?? 'http://placekitten.com/200/300' }}
style={{ height: 40, width: 40, borderRadius: 20, marginRight: 10 }}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion apps/arcade-chat/components/ChannelList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Channel, useChannelsCreated } from '@arcadecity/use-arcade'
import { FlatList, StyleSheet, Text } from 'react-native'
import { FlatList, StyleSheet } from 'react-native'
import { ChannelPreview } from './ChannelPreview'

export const ChannelList = () => {
Expand Down
13 changes: 9 additions & 4 deletions apps/arcade-chat/components/ChannelPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { color, palette, spacing, typography } from '@arcadecity/ui'
import { Channel, setActiveChannelId, useLastChannelMessage } from '@arcadecity/use-arcade'
import {
Channel,
setActiveChannelId,
useChannelMetadata,
useLastChannelMessage,
} from '@arcadecity/use-arcade'
import { useNavigation } from '@react-navigation/native'
import { useCallback } from 'react'
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
Expand All @@ -11,18 +16,18 @@ export const ChannelPreview = ({ channel }: { channel: Channel }) => {
setActiveChannelId(channel.id)
navigation.navigate('channel', { channelId: channel.id })
}, [channel.id])

const lastMessage = useLastChannelMessage(channel.id)
const lastMessageText = lastMessage?.text ?? '-'
const metadata = useChannelMetadata(channel.id)
return (
<TouchableOpacity
activeOpacity={0.8}
key={channel.id}
onPress={navToIt}
style={styles.container}>
<ChannelAvatar channel={channel} />
<ChannelAvatar metadata={metadata} />
<View style={styles.contentContainer}>
<Text style={styles.channelName}>{channel.name}</Text>
<Text style={styles.channelName}>{metadata.name}</Text>
<Text style={styles.channelPreview}>{lastMessageText}</Text>
</View>
</TouchableOpacity>
Expand Down
6 changes: 5 additions & 1 deletion apps/arcade-chat/components/MessageInput.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { color, palette } from '@arcadecity/ui'
import { Alert, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native'
import { FontAwesome } from '@expo/vector-icons'
import { useContext, useState } from 'react'
import { useContext, useRef, useState } from 'react'
import { ArcadeContext, useActiveChannelId, UseArcadeRelayActions } from '@arcadecity/use-arcade'

export const MessageInput = () => {
const [text, setText] = useState('')
const context = useContext(ArcadeContext)
const activeChannelId = useActiveChannelId()
const actions = context.actions as UseArcadeRelayActions
const inputBoxRef = useRef<TextInput | null>(null)
const submitInput = () => {
if (text.length < 1) {
Alert.alert('Message too short', 'What is that, a message for ants?')
Expand All @@ -18,6 +19,8 @@ export const MessageInput = () => {
Alert.alert('Error getting channel ID')
return
}
inputBoxRef.current?.clear()
setText('')
actions.sendChannelMessage(activeChannelId, text)
}
return (
Expand All @@ -28,6 +31,7 @@ export const MessageInput = () => {
autoCorrect={false}
multiline
onChangeText={(text: string) => setText(text)}
ref={inputBoxRef}
spellCheck={false}
style={styles.inputBox}
/>
Expand Down
12 changes: 0 additions & 12 deletions apps/arcade-chat/components/MessagePreview.tsx

This file was deleted.

5 changes: 2 additions & 3 deletions apps/arcade-chat/components/message/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ interface Props {

export const MessagePreview: React.FC<Props> = ({ message, preset }) => {
const text = message.text
const username = `Anon-${message.pubkey.trim().substr(0, 5)}`
const username = `Anon-${message.pubkey.slice(0, 5)}`
const date = message.created_at * 1000
const twohundy = Math.round(200 + Math.random() * 5)
const photo = `https://placekitten.com/200/${twohundy.toString()}`
const photo = `https://placekitten.com/200/201`

const delivered = true
const messagePreset: any = messagePresets[preset]
Expand Down
34 changes: 33 additions & 1 deletion apps/arcade-chat/navigation/chat-navigator.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import React from 'react'
import React, { useContext } from 'react'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import TabOneScreen from '../screens/TabOneScreen'
import { ChannelScreen } from '../screens/ChannelScreen'
import { stackOptions } from './stackOptions'
import { useNavigation } from '@react-navigation/native'
import { NavButton } from '../components/nav-button'
import { Pressable } from 'react-native'
import { FontAwesome } from '@expo/vector-icons'
import { palette } from '@arcadecity/ui'
import {
ArcadeContext,
formatEvent,
updateDemoChannelMetadata,
useActiveChannelId,
} from '@arcadecity/use-arcade'

const Stack = createNativeStackNavigator()

export const ChatNavigator = () => {
const navigation = useNavigation()
const activeChannelId = useActiveChannelId()
const context = useContext(ArcadeContext)
const demoUpdateMetadata = async () => {
if (!activeChannelId) return
const metadataEvent = await updateDemoChannelMetadata(activeChannelId)
const formattedEvent = formatEvent(metadataEvent)
context.ws.send(formattedEvent)
}
return (
<Stack.Navigator initialRouteName='chathome'>
<Stack.Screen
Expand All @@ -24,6 +41,21 @@ export const ChatNavigator = () => {
...stackOptions,
title: 'Channel',
headerLeft: () => <NavButton onPress={navigation.goBack} />,
headerRight: () => (
<Pressable
// onPress={demoUpdateMetadata}
onPress={() => navigation.navigate('Modal')}
style={({ pressed }) => ({
opacity: pressed ? 0.5 : 1,
})}>
<FontAwesome
name='info-circle'
size={25}
color={palette.moonRaker}
style={{ marginRight: 15 }}
/>
</Pressable>
),
}}
/>
{/* <Stack.Screen name='profile' component={Profile} options={stackOptions} /> */}
Expand Down
2 changes: 1 addition & 1 deletion apps/arcade-chat/navigation/root-navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function RootNavigator() {
<Stack.Screen
name='Modal'
component={ModalScreen}
options={{ ...stackOptions, title: 'Info' }}
options={{ ...stackOptions, title: 'Update Channel Metadata' }}
/>
</Stack.Group>
</Stack.Navigator>
Expand Down
22 changes: 16 additions & 6 deletions apps/arcade-chat/screens/ChannelScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { useChannelMetadata } from '@arcadecity/use-arcade'
import { useEffect } from 'react'
import { View } from 'react-native'
import { MessageInput } from '../components/MessageInput'
import { MessageList } from '../components/MessageList'
import { RootStackScreenProps } from '../types'

export const ChannelScreen = () => (
<View style={{ flex: 1, width: '100%' }}>
<MessageList />
<MessageInput />
</View>
)
export const ChannelScreen = ({ navigation }: RootStackScreenProps<'channel'>) => {
const metadata = useChannelMetadata()
// console.log(metadata)
useEffect(() => {
navigation.setOptions({ title: metadata.name })
}, [metadata])
return (
<View style={{ flex: 1, width: '100%' }}>
<MessageList />
<MessageInput />
</View>
)
}
87 changes: 71 additions & 16 deletions apps/arcade-chat/screens/ModalScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,90 @@
import { StatusBar } from 'expo-status-bar';
import { Platform, StyleSheet } from 'react-native';

import EditScreenInfo from '../components/EditScreenInfo';
import { Text, View } from '../components/Themed';
import { color } from '@arcadecity/ui'
import {
ArcadeContext,
formatEvent,
updateChannelMetadata,
useChannelMetadata,
} from '@arcadecity/use-arcade'
import { useNavigation } from '@react-navigation/native'
import { useContext, useRef, useState } from 'react'
import { Button, StyleSheet, TextInput } from 'react-native'
import { Text, View } from '../components/Themed'

export default function ModalScreen() {
const metadata = useChannelMetadata()
const navigation = useNavigation()
const [name, setName] = useState(metadata.name)
const [picture, setPicture] = useState(metadata.picture)
const inputBoxRef = useRef<TextInput | null>(null)
const context = useContext(ArcadeContext)
const updateMetadata = async () => {
// TODO: get channelID properly now that we are storing channelId in tag not prop
const event = await updateChannelMetadata(metadata.tags[0][1], { name, picture })
const formattedEvent = formatEvent(event)
context.ws.send(formattedEvent)
navigation.goBack()
}
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="/screens/ModalScreen.tsx" />

{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
<Text style={styles.title}>Name</Text>
<View style={styles.inputContainer}>
<TextInput
autoCorrect={false}
defaultValue={metadata.name}
onChangeText={(text: string) => setName(text)}
ref={inputBoxRef}
spellCheck={false}
style={styles.inputBox}
/>
</View>
<View style={styles.inputContainer}>
<TextInput
autoCorrect={false}
defaultValue={metadata.picture}
onChangeText={(text: string) => setPicture(text)}
ref={inputBoxRef}
spellCheck={false}
style={styles.inputBox}
/>
</View>
<Button title='Update channel metadata' onPress={updateMetadata} />
</View>
);
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color.background,
},
title: {
fontSize: 20,
fontWeight: 'bold',
inputBox: {
backgroundColor: color.field,
color: color.text,
flexGrow: 1,
fontSize: 14,
height: 40,
borderRadius: 10,
includeFontPadding: false,
padding: 10,
textAlignVertical: 'center',
},
inputContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
padding: 20,
backgroundColor: color.background,
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});
title: {
color: color.text,
fontSize: 20,
fontWeight: 'bold',
},
})
44 changes: 44 additions & 0 deletions packages/use-arcade/src/demo/updateDemoChannelMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
createNewAccount,
getEventHash,
NostrEvent,
NostrEventToSerialize,
NostrEventToSign,
NostrKind,
signEvent,
} from '../nostr'
import { ChannelMetadata } from '../store'

export const updateDemoChannelMetadata = async (channelId: string) => {
const { pubkey, privkey } = createNewAccount()
const date = new Date()
const dateTimeInSeconds = Math.floor(date.getTime() / 1000)
const twohundy = Math.round(200 + Math.random() * 5)
const photo = `https://placekitten.com/200/${twohundy.toString()}`
const channelMetadata = {
about: `test about metadadtatatata ${twohundy}`,
channelId,
name: `test name ${twohundy}`,
picture: photo,
type: 'public', // maybe 'public', 'private', 'geohash', 'geocoords'
}
const nostrEventToSerialize: NostrEventToSerialize = {
created_at: dateTimeInSeconds,
kind: NostrKind.channelmetadata,
tags: [['#e', channelId]],
content: JSON.stringify(channelMetadata),
pubkey,
}
const id = getEventHash(nostrEventToSerialize)
const nostrEventToSign: NostrEventToSign = {
...nostrEventToSerialize,
id,
}
const sig = await signEvent(nostrEventToSign, privkey)
const nostrEvent: NostrEvent = {
...nostrEventToSerialize,
id,
sig,
} as ChannelMetadata
return nostrEvent
}
4 changes: 4 additions & 0 deletions packages/use-arcade/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './useActiveChannelId'
export * from './useArcadeRelay'
export * from './useChannelMessages'
export * from './useChannelMetadata'
export * from './useChannelsCreated'
export * from './useDebounce'
export * from './useLastChannelMessage'
7 changes: 7 additions & 0 deletions packages/use-arcade/src/hooks/useActiveChannelId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useSnapshot } from 'valtio'
import { store } from '../store'

export const useActiveChannelId: () => string | null = () => {
const snapshot = useSnapshot(store)
return snapshot.activeChannelId
}
Loading

1 comment on commit a33b06d

@vercel
Copy link

@vercel vercel bot commented on a33b06d Aug 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.