Skip to content

Commit 18b0681

Browse files
committed
Refactor ChatScreen to split off packet handling.
1 parent 1f7cd8e commit 18b0681

File tree

3 files changed

+160
-126
lines changed

3 files changed

+160
-126
lines changed

src/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import SettingsContext, {
2828
} from './context/settingsContext'
2929
import ServersContext, { Servers } from './context/serversContext'
3030
import DisconnectDialog from './components/DisconnectDialog'
31-
import ChatScreen from './screens/ChatScreen'
31+
import ChatScreen from './screens/chat/ChatScreen'
3232
import ServerScreen from './screens/ServerScreen'
3333
import AccountScreen from './screens/accounts/AccountScreen'
3434
import SettingScreen from './screens/settings/SettingScreen'

src/screens/ChatScreen.tsx renamed to src/screens/chat/ChatScreen.tsx

+39-125
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,27 @@ import {
1111
import Ionicons from 'react-native-vector-icons/Ionicons'
1212
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
1313

14-
import globalStyle from '../globalStyle'
15-
import useDarkMode from '../context/useDarkMode'
16-
import SettingsContext from '../context/settingsContext'
17-
import ConnectionContext from '../context/connectionContext'
14+
import packetHandler from './packetHandler'
15+
import globalStyle from '../../globalStyle'
16+
import useDarkMode from '../../context/useDarkMode'
17+
import SettingsContext from '../../context/settingsContext'
18+
import ConnectionContext from '../../context/connectionContext'
1819
import {
1920
ChatToJsx,
20-
parseValidJson,
2121
mojangColorMap,
2222
lightColorMap,
2323
MinecraftChat,
2424
ClickEvent,
2525
ColorMap
26-
} from '../minecraft/chatToJsx'
27-
import { protocolMap, readVarInt, writeVarInt } from '../minecraft/utils'
28-
import { concatPacketData } from '../minecraft/packet'
29-
import TextField from '../components/TextField'
30-
import Text from '../components/Text'
26+
} from '../../minecraft/chatToJsx'
27+
import { protocolMap, writeVarInt } from '../../minecraft/utils'
28+
import { concatPacketData } from '../../minecraft/packet'
29+
import TextField from '../../components/TextField'
30+
import Text from '../../components/Text'
31+
import SettingScreen from '../settings/SettingScreen'
3132

32-
import SettingScreen from './settings/SettingScreen'
33+
const enderChatPrefix = '\u00A74[\u00A7cEnderChat\u00A74] \u00A7c'
34+
const sendMessageErr = 'Failed to send message to server!'
3335

3436
type ChatNavigationProp = NativeStackNavigationProp<
3537
{ Home: undefined; Chat: undefined },
@@ -40,22 +42,20 @@ interface Message {
4042
text: MinecraftChat
4143
}
4244

43-
const renderItem = (
44-
colorMap: ColorMap,
45-
clickEventHandler: (ce: ClickEvent) => void
46-
) => {
45+
const renderItem = (colorMap: ColorMap, handleCe: (ce: ClickEvent) => void) => {
4746
const ItemRenderer = ({ item }: { item: Message }) => (
4847
<View style={styles.androidScaleInvert}>
4948
<ChatToJsx
5049
chat={item.text}
5150
component={Text}
5251
colorMap={colorMap}
53-
clickEventHandler={clickEventHandler}
52+
clickEventHandler={handleCe}
5453
/>
5554
</View>
5655
)
57-
return ItemRenderer // LOW-TODO: Performance implications?
58-
} // https://reactnative.dev/docs/optimizing-flatlist-configuration
56+
// LOW-TODO: Performance implications? https://reactnative.dev/docs/optimizing-flatlist-configuration
57+
return ItemRenderer
58+
}
5959
const ChatMessageList = (props: {
6060
messages: Message[]
6161
colorMap: ColorMap
@@ -73,15 +73,7 @@ const ChatMessageList = (props: {
7373
}
7474
const ChatMessageListMemo = React.memo(ChatMessageList) // Shallow prop compare.
7575

76-
const enderChatPrefix = '\u00A74[\u00A7cEnderChat\u00A74] \u00A7c'
77-
const sendMessageErr = 'Failed to send message to server!'
78-
const parseMessageErr = 'An error occurred when parsing chat.'
79-
const inventoryCloseErr = 'An error occurred when closing an inventory window.'
80-
const respawnErr = 'An error occurred when trying to respawn after death.'
81-
const deathRespawnMessage = enderChatPrefix + 'You died! Respawning...'
82-
const healthMessage =
83-
enderChatPrefix + "You're losing health! \u00A7b%prev \u00A7f-> \u00A7c%new"
84-
const errorHandler =
76+
const handleError =
8577
(addMessage: (text: MinecraftChat) => void, translated: string) =>
8678
(error: unknown) => {
8779
console.error(error)
@@ -99,11 +91,8 @@ const ChatScreen = ({ navigation }: { navigation: ChatNavigationProp }) => {
9991
const [messages, setMessages] = useState<Message[]>([])
10092
const [loggedIn, setLoggedIn] = useState(false)
10193
const [message, setMessage] = useState('')
102-
const loggedInRef = useRef(false)
10394
const idRef = useRef(0)
10495

105-
const healthRef = useRef<number | null>(null)
106-
10796
const charLimit =
10897
connection && connection.connection.options.protocolVersion >= 306 // 16w38a
10998
? 256
@@ -115,98 +104,25 @@ const ChatScreen = ({ navigation }: { navigation: ChatNavigationProp }) => {
115104
})
116105

117106
// Packet handler useEffect.
107+
const loggedInRef = useRef(false)
108+
const healthRef = useRef<number | null>(null)
118109
useEffect(() => {
119110
if (!connection) return
120-
connection.connection.on('packet', packet => {
121-
if (!loggedInRef.current && connection.connection.loggedIn) {
122-
setLoggedIn(true)
123-
loggedInRef.current = true
124-
if (settings.sendJoinMessage) {
125-
connection.connection
126-
.writePacket(
127-
0x03,
128-
concatPacketData([settings.joinMessage.substring(0, charLimit)])
129-
)
130-
.catch(errorHandler(addMessage, sendMessageErr))
131-
}
132-
if (settings.sendSpawnCommand) {
133-
connection.connection
134-
.writePacket(0x03, concatPacketData(['/spawn']))
135-
.catch(errorHandler(addMessage, sendMessageErr))
136-
}
137-
} else if (
138-
packet.id === 0x0f /* Chat Message (clientbound) */ &&
139-
connection.connection.options.protocolVersion < protocolMap['1.19']
140-
) {
141-
try {
142-
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
143-
const chatJson = packet.data
144-
.slice(chatVarIntLength, chatVarIntLength + chatLength)
145-
.toString('utf8')
146-
const position = packet.data.readInt8(chatVarIntLength + chatLength)
147-
// TODO: Support position 2 (also in 0x5f packet) and sender for disableChat/blocked players.
148-
if (position === 0 || position === 1) {
149-
addMessage(parseValidJson(chatJson))
150-
}
151-
} catch (e) {
152-
errorHandler(addMessage, parseMessageErr)(e)
153-
}
154-
} else if (
155-
packet.id === 0x30 /* Player Chat Message (clientbound) */ &&
156-
connection.connection.options.protocolVersion >= protocolMap['1.19']
157-
) {
158-
// TODO-1.19: Support player chat messages.
159-
} else if (
160-
packet.id === 0x5f /* System Chat Message (clientbound) */ &&
161-
connection.connection.options.protocolVersion >= protocolMap['1.19']
162-
) {
163-
try {
164-
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
165-
const chatJson = packet.data
166-
.slice(chatVarIntLength, chatVarIntLength + chatLength)
167-
.toString('utf8')
168-
const position = packet.data.readInt8(chatVarIntLength + chatLength)
169-
// TODO-1.19 - 3: say command, 4: msg command, 5: team msg command, 6: emote command, 7: tellraw command
170-
if (position === 0 || position === 1) {
171-
addMessage(parseValidJson(chatJson))
172-
}
173-
} catch (e) {
174-
errorHandler(addMessage, parseMessageErr)(e)
175-
}
176-
} else if (packet.id === 0x2e /* Open Window */) {
177-
// Just close the window.
178-
const [windowId] = readVarInt(packet.data)
179-
const buf = Buffer.alloc(1)
180-
buf.writeUInt8(windowId)
181-
connection.connection // Close Window (serverbound)
182-
.writePacket(0x09, buf)
183-
.catch(errorHandler(addMessage, inventoryCloseErr))
184-
} else if (packet.id === 0x35 /* Death Combat Event */) {
185-
const [, playerIdLen] = readVarInt(packet.data)
186-
const offset = playerIdLen + 4 // Entity ID
187-
const [chatLen, chatVarIntLength] = readVarInt(packet.data, offset)
188-
const jsonOffset = offset + chatVarIntLength
189-
const deathMessage = parseValidJson(
190-
packet.data.slice(jsonOffset, jsonOffset + chatLen).toString('utf8')
191-
)
192-
addMessage(deathRespawnMessage)
193-
if (deathMessage?.text || deathMessage?.extra) addMessage(deathMessage)
194-
// Automatically respawn.
195-
// LOW-TODO: Should this be manual, or a dialog, like MC?
196-
connection.connection // Client Status
197-
.writePacket(0x04, writeVarInt(0))
198-
.catch(errorHandler(addMessage, respawnErr))
199-
} else if (packet.id === 0x52 /* Update Health */) {
200-
const newHealth = packet.data.readFloatBE(0)
201-
if (healthRef.current != null && healthRef.current > newHealth) {
202-
const info = healthMessage
203-
.replace('%prev', healthRef.current.toString())
204-
.replace('%new', newHealth.toString())
205-
addMessage(info)
206-
} // LOW-TODO: Long-term it would be better to have a UI.
207-
healthRef.current = newHealth
208-
}
209-
})
111+
connection.connection.on(
112+
'packet',
113+
packetHandler(
114+
healthRef,
115+
loggedInRef,
116+
setLoggedIn,
117+
connection.connection,
118+
addMessage,
119+
settings.joinMessage,
120+
settings.sendJoinMessage,
121+
settings.sendSpawnCommand,
122+
handleError,
123+
charLimit
124+
)
125+
)
210126
return () => {
211127
connection.connection.removeAllListeners('packet')
212128
}
@@ -240,15 +156,15 @@ const ChatScreen = ({ navigation }: { navigation: ChatNavigationProp }) => {
240156
if (connection.connection.options.protocolVersion < protocolMap['1.19']) {
241157
connection.connection
242158
.writePacket(0x03, concatPacketData([msg]))
243-
.catch(errorHandler(addMessage, sendMessageErr))
159+
.catch(handleError(addMessage, sendMessageErr))
244160
} else {
245161
const timestamp = Buffer.alloc(8)
246162
connection.connection
247163
.writePacket(
248164
0x04,
249165
concatPacketData([msg, timestamp, writeVarInt(0), false])
250166
)
251-
.catch(errorHandler(addMessage, sendMessageErr))
167+
.catch(handleError(addMessage, sendMessageErr))
252168
// TODO-1.19: Support sending Chat Command/Chat Message/Chat Preview.
253169
}
254170
}
@@ -351,9 +267,7 @@ const styles = StyleSheet.create({
351267
backButton: { marginRight: 8 },
352268
backButtonIcon: { marginRight: 0 },
353269
sendButtonIcon: { marginRight: 0, marginLeft: 4 },
354-
androidScaleInvert: {
355-
scaleY: Platform.OS === 'android' ? -1 : undefined
356-
},
270+
androidScaleInvert: { scaleY: Platform.OS === 'android' ? -1 : undefined },
357271
chatArea: { padding: 8, flex: 1, scaleY: -1 },
358272
chatAreaScrollView: { paddingBottom: 16 },
359273
loadingScreen: { flex: 1, alignItems: 'center', justifyContent: 'center' },

src/screens/chat/packetHandler.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React from 'react'
2+
import { MinecraftChat, parseValidJson } from '../../minecraft/chatToJsx'
3+
import { ServerConnection } from '../../minecraft/connection'
4+
import { concatPacketData, Packet } from '../../minecraft/packet'
5+
import { protocolMap, readVarInt, writeVarInt } from '../../minecraft/utils'
6+
7+
const enderChatPrefix = '\u00A74[\u00A7cEnderChat\u00A74] \u00A7c'
8+
const parseMessageErr = 'An error occurred when parsing chat.'
9+
const inventoryCloseErr = 'An error occurred when closing an inventory window.'
10+
const respawnErr = 'An error occurred when trying to respawn after death.'
11+
const deathRespawnMessage = enderChatPrefix + 'You died! Respawning...'
12+
const sendMessageErr = 'Failed to send message to server!'
13+
const healthMessage =
14+
enderChatPrefix + "You're losing health! \u00A7b%prev \u00A7f-> \u00A7c%new"
15+
16+
export default (
17+
healthRef: React.MutableRefObject<number | null>,
18+
loggedInRef: React.MutableRefObject<boolean>,
19+
setLoggedIn: React.Dispatch<React.SetStateAction<boolean>>,
20+
connection: ServerConnection,
21+
addMessage: (text: MinecraftChat) => any,
22+
joinMessage: string,
23+
sendJoinMessage: boolean,
24+
sendSpawnCommand: boolean,
25+
handleError: (
26+
addMsg: (text: MinecraftChat) => void,
27+
translated: string
28+
) => (error: unknown) => any,
29+
charLimit: number
30+
) =>
31+
(packet: Packet) => {
32+
if (!loggedInRef.current && connection.loggedIn) {
33+
setLoggedIn(true)
34+
loggedInRef.current = true
35+
if (sendJoinMessage) {
36+
connection
37+
.writePacket(
38+
0x03,
39+
concatPacketData([joinMessage.substring(0, charLimit)])
40+
)
41+
.catch(handleError(addMessage, sendMessageErr))
42+
}
43+
if (sendSpawnCommand) {
44+
connection
45+
.writePacket(0x03, concatPacketData(['/spawn']))
46+
.catch(handleError(addMessage, sendMessageErr))
47+
}
48+
}
49+
50+
const is119 = connection.options.protocolVersion >= protocolMap['1.19']
51+
if (packet.id === 0x0f /* Chat Message (clientbound) */ && !is119) {
52+
try {
53+
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
54+
const chatJson = packet.data
55+
.slice(chatVarIntLength, chatVarIntLength + chatLength)
56+
.toString('utf8')
57+
const position = packet.data.readInt8(chatVarIntLength + chatLength)
58+
// TODO: Support position 2 (also in 0x5f packet) and sender for disableChat/blocked players.
59+
if (position === 0 || position === 1) {
60+
addMessage(parseValidJson(chatJson))
61+
}
62+
} catch (e) {
63+
handleError(addMessage, parseMessageErr)(e)
64+
}
65+
} else if (
66+
packet.id === 0x30 /* Player Chat Message (clientbound) */ &&
67+
is119
68+
) {
69+
// TODO-1.19: Support player chat messages.
70+
} else if (
71+
packet.id === 0x5f /* System Chat Message (clientbound) */ &&
72+
is119
73+
) {
74+
try {
75+
const [chatLength, chatVarIntLength] = readVarInt(packet.data)
76+
const chatJson = packet.data
77+
.slice(chatVarIntLength, chatVarIntLength + chatLength)
78+
.toString('utf8')
79+
const position = packet.data.readInt8(chatVarIntLength + chatLength)
80+
// TODO-1.19 - 3: say command, 4: msg command, 5: team msg command, 6: emote command, 7: tellraw command
81+
if (position === 0 || position === 1) {
82+
addMessage(parseValidJson(chatJson))
83+
}
84+
} catch (e) {
85+
handleError(addMessage, parseMessageErr)(e)
86+
}
87+
} else if (packet.id === 0x2e /* Open Window */) {
88+
// Just close the window.
89+
const [windowId] = readVarInt(packet.data)
90+
const buf = Buffer.alloc(1)
91+
buf.writeUInt8(windowId)
92+
connection // Close Window (serverbound)
93+
.writePacket(0x09, buf)
94+
.catch(handleError(addMessage, inventoryCloseErr))
95+
} else if (packet.id === 0x35 /* Death Combat Event */) {
96+
const [, playerIdLen] = readVarInt(packet.data)
97+
const offset = playerIdLen + 4 // Entity ID
98+
const [chatLen, chatVarIntLength] = readVarInt(packet.data, offset)
99+
const jsonOffset = offset + chatVarIntLength
100+
const deathMessage = parseValidJson(
101+
packet.data.slice(jsonOffset, jsonOffset + chatLen).toString('utf8')
102+
)
103+
addMessage(deathRespawnMessage)
104+
if (deathMessage?.text || deathMessage?.extra) addMessage(deathMessage)
105+
// Automatically respawn.
106+
// LOW-TODO: Should this be manual, or a dialog, like MC?
107+
connection // Client Status
108+
.writePacket(0x04, writeVarInt(0))
109+
.catch(handleError(addMessage, respawnErr))
110+
} else if (packet.id === 0x52 /* Update Health */) {
111+
const newHealth = packet.data.readFloatBE(0)
112+
if (healthRef.current != null && healthRef.current > newHealth) {
113+
const info = healthMessage
114+
.replace('%prev', healthRef.current.toString())
115+
.replace('%new', newHealth.toString())
116+
addMessage(info)
117+
} // LOW-TODO: Long-term it would be better to have a UI.
118+
healthRef.current = newHealth
119+
}
120+
}

0 commit comments

Comments
 (0)