Skip to content

Commit

Permalink
feat: add message entering animation
Browse files Browse the repository at this point in the history
  • Loading branch information
std-microblock committed Apr 6, 2024
1 parent 8f74521 commit 240da6d
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 77 deletions.
14 changes: 10 additions & 4 deletions src/satori/protocol.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Alert } from 'react-native'
import { ConnectionInfo } from './connection'
import Element from './element'
import { Dict } from 'cosmokit'
Expand Down Expand Up @@ -209,6 +210,11 @@ const fixArrays = (obj: object) => {
}
}

const satoriError = (message: string) => {
Alert.alert('Satori Error', message)
// throw new Error(message)
}

/*
HTTP API
这是一套 HTTP RPC 风格的 API,所有 URL 的形式均为 /{path}/{version}/{resource}.{method}。其中,path 为部署路径 (可以为空),version 为 API 的版本号,resource 是资源类型,method 为方法名。
Expand All @@ -234,11 +240,11 @@ export const callMethodAsync = (method: string, args: object, connectionInfo: Co
// Verify fields
const methodInfo = Methods[method]
if (!methodInfo) {
throw new Error(`Unknown method ${method}`)
satoriError(`Unknown method ${method}`)
}
for (const key in args) {
if (!methodInfo.fields.some(field => field.name === key)) {
throw new Error(`Redundant field ${key} in args`)
satoriError(`Redundant field ${key} in args`)
}
}

Expand All @@ -256,15 +262,15 @@ export const callMethodAsync = (method: string, args: object, connectionInfo: Co
console.log('call satori', method)
return fetch(url, { method: 'POST', headers, body }).then(res => {
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText} (${httpCodeTips[res.status]})`)
satoriError(`HTTP ${res.status}: ${res.statusText} (${httpCodeTips[res.status]})`)
}
return res.text()
}).then(v => {
try {
console.log(method, '=> response', v)
return fixArrays(convertSnakeObjectToCamel(JSON.parse(v)))
} catch (e) {
throw new Error(`Failed to parse response: ${v}`)
satoriError(`Failed to parse response: ${v}`)
}
})
}
Expand Down
170 changes: 97 additions & 73 deletions src/screens/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { elementRendererMap, renderElement, elementToObject, toPreviewString } f
import React from "react"
import Clipboard from "@react-native-clipboard/clipboard"
import { create } from "zustand"
import Animated, { Easing, FadeIn, Keyframe, LinearTransition, useSharedValue, withTiming } from "react-native-reanimated"

const useReplyTo = create<{
replyTo: SaMessage | null,
Expand All @@ -23,7 +24,7 @@ const useReplyTo = create<{
const Message = memo(({ message }: { message: SaMessage }) => {
const content = useMemo(() => Element.parse(message.content).map(elementToObject), [message]);
const login = useLogin()
const isSelf = login.selfId === message.user?.id
const isSelf = login?.selfId === message.user?.id
const [menuVisible, setMenuVisible] = useState(false)
const [menuAnchor, setMenuAnchor] = useState<{
x: number,
Expand All @@ -34,81 +35,104 @@ const Message = memo(({ message }: { message: SaMessage }) => {

const { setReplyTo } = useReplyTo()

return <TouchableRipple style={{
marginVertical: 10,
alignItems: isSelf ? 'flex-end' : 'baseline'
}} onPress={e => {
setMenuAnchor({
x: e.nativeEvent.pageX,
y: e.nativeEvent.pageY
})
setMenuVisible(true)
}}>
<>
const animMaxHeight = useSharedValue(0);

<View style={{
flexDirection: isSelf ? 'row-reverse' : 'row',
gap: 10,
}}>
{message.user ? <><Avatar.Image source={{ uri: message.user.avatar }} size={20} />
<Text>{message.user.name ?? message.user.id}</Text></> : <Text>Unknown user</Text>}
</View>
<Card style={{
marginVertical: 8,
paddingHorizontal: 15,
padding: 10,
borderRadius: 20
}}>
{
content.map(renderElement)
}
</Card>

<Menu
anchor={menuAnchor}
visible={menuVisible}
onDismiss={() => setMenuVisible(false)}
>
<Menu.Item onPress={async () => {
setMenuVisible(false)

await satori.bot.createMessage(message.channel.id, message.content, message.guild.id)
}} title="+1" />
<Menu.Item onPress={() => {
setMenuVisible(false)

const content = Element.parse(message.content)
const text = content.map(v => v.attrs?.text).filter(v => v).join(' ')

Clipboard.setString(text)
ToastAndroid.show('已复制', ToastAndroid.SHORT)
}} title="复制" />
<Menu.Item onPress={() => {
setMenuVisible(false)

setMsgStore(msgStore => {
msgStore[message.channel.id] = msgStore[message.channel.id].filter(v => v.id !== message.id)
return { ...msgStore }
})

ToastAndroid.show('已删除', ToastAndroid.SHORT)
}} title="删除" />
<Menu.Item onPress={() => {
setMenuVisible(false)

setReplyTo(message)
}} title="回复" />
<Menu.Item onPress={() => {
setMenuVisible(false)
const inspect = v => JSON.stringify(v, null, 4)
Alert.alert('消息信息',
`Sender ${inspect(message.user)}
useEffect(() => {
if ((Date.now() - message.timestamp * 1000) < 1000) {
animMaxHeight.value = withTiming(500, {
duration: 300,
easing: Easing.linear
})

setTimeout(() => {
animMaxHeight.value = 2000
}, 1000)
}
else
animMaxHeight.value = 2000
}, [message])

return <Animated.View style={{
// height: 20
maxHeight: animMaxHeight,
overflow: 'visible'
}}>
<TouchableRipple style={{
marginVertical: 10,
alignItems: isSelf ? 'flex-end' : 'baseline',
height: 'auto'
}} onPress={e => {
setMenuAnchor({
x: e.nativeEvent.pageX,
y: e.nativeEvent.pageY
})
setMenuVisible(true)
}}>
<>
<View style={{
flexDirection: isSelf ? 'row-reverse' : 'row',
gap: 10,
}}>
{message.user ? <><Avatar.Image source={{ uri: message.user.avatar }} size={20} />
<Text>{message.user.name ?? message.user.id}</Text></> : <Text>Unknown user</Text>}
</View>
<Card style={{
marginVertical: 8,
paddingHorizontal: 15,
padding: 10,
borderRadius: 20
}}>
{
content.map(renderElement)
}
</Card>

<Menu
anchor={menuAnchor}
visible={menuVisible}
onDismiss={() => setMenuVisible(false)}
>
<Menu.Item onPress={async () => {
setMenuVisible(false)

await satori.bot.createMessage(message.channel.id, message.content, message.guild.id)
}} title="+1" />
<Menu.Item onPress={() => {
setMenuVisible(false)

const content = Element.parse(message.content)
const text = content.map(v => v.attrs?.text).filter(v => v).join(' ')

Clipboard.setString(text)
ToastAndroid.show('已复制', ToastAndroid.SHORT)
}} title="复制" />
<Menu.Item onPress={() => {
setMenuVisible(false)

setMsgStore(msgStore => {
msgStore[message.channel.id] = msgStore[message.channel.id].filter(v => v.id !== message.id)
return { ...msgStore }
})

ToastAndroid.show('已删除', ToastAndroid.SHORT)
}} title="删除" />
<Menu.Item onPress={() => {
setMenuVisible(false)

setReplyTo(message)
}} title="回复" />
<Menu.Item onPress={() => {
setMenuVisible(false)
const inspect = v => JSON.stringify(v, null, 4)
Alert.alert('消息信息',
`Sender ${inspect(message.user)}
Channel ${inspect(message.channel)}
Content ${inspect(content)}`)
}} title="详细信息" />
</Menu>
</>
</TouchableRipple>
}} title="详细信息" />
</Menu>
</>
</TouchableRipple >
</Animated.View>
})

export const Chat = ({
Expand Down

0 comments on commit 240da6d

Please sign in to comment.