Skip to content
This repository has been archived by the owner on Aug 3, 2023. It is now read-only.

Commit

Permalink
feat: 聊天记录页
Browse files Browse the repository at this point in the history
  • Loading branch information
mys1024 committed Mar 2, 2023
1 parent f082160 commit e822a72
Show file tree
Hide file tree
Showing 19 changed files with 619 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"unocss.root": "src/renderer",
"cSpell.words": [
"bodyparser",
"daterange",
"datetime",
"datetimerange",
"pointlink",
"unlinkable"
]
Expand Down
9 changes: 7 additions & 2 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { resolve } from 'node:path'
import { pathExists } from 'fs-extra'
import { ipcMain, shell } from 'electron'

import type { Message, NetworkInterface, NetworkInterfaceInfo } from './typings/app'
import type { Message, NetworkInterface, NetworkInterfaceInfo, RtcSignal } from './typings/app'
import { mainWindowPromise } from './main'
import { getObservedIp } from './utils/net'
import { MESSAGE_SERVER_PORT } from './message/server'
import { MESSAGE_SERVER_PORT } from './receiver/server'

ipcMain.handle('get-observed-ip', async (event, family: 4 | 6) => {
return await getObservedIp(family)
Expand Down Expand Up @@ -48,3 +48,8 @@ export async function sendNewMessageToMainWindow(message: Message) {
const mainWindow = await mainWindowPromise
mainWindow.webContents.send('new-message', message)
}

export async function sendRtcSignalToMainWindow(signal: RtcSignal) {
const mainWindow = await mainWindowPromise
mainWindow.webContents.send('rtc-signal', signal)
}
2 changes: 1 addition & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from 'path'
import { BrowserWindow, Menu, app, globalShortcut, session } from 'electron'
import './ipc'
import './message/server'
import './receiver/server'

async function start() {
await app.whenReady()
Expand Down
7 changes: 6 additions & 1 deletion src/main/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron'
import type { Message } from './typings/app'
import type { Message, RtcSignal } from './typings/app'

contextBridge.exposeInMainWorld('electron', {
async getObservedIp(family: 4 | 6) {
Expand All @@ -22,4 +22,9 @@ contextBridge.exposeInMainWorld('electron', {
showItemInfolder(path: string) {
ipcRenderer.send('show-item-in-folder', path)
},
setRtcSignalHandler(handler: (signal: RtcSignal) => void) {
ipcRenderer.on('rtc-signal', (event, signal) => {
handler(signal)
})
},
})
File renamed without changes.
12 changes: 12 additions & 0 deletions src/main/message/router.ts → src/main/receiver/router.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import fse from 'fs-extra'
import Router from '@koa/router'
import type { RtcOfferSignal } from '../typings/app'
import {
sendNewMessageToMainWindow,
sendRtcSignalToMainWindow,
} from '../ipc'

const router = new Router()
Expand Down Expand Up @@ -160,4 +162,14 @@ router.post('/message/file', async (ctx) => {
ctx.status = 200
})

router.post('/rtc/signal', async (ctx) => {
// 获取参数
const body = ctx.request.body
const offerSignal = body.signal as RtcOfferSignal // TODO: 类型校验
// 发送 WebRTC offer 到渲染进程
await sendRtcSignalToMainWindow(offerSignal)
// 响应
ctx.status = 200
})

export default router
2 changes: 1 addition & 1 deletion src/main/message/server.ts → src/main/receiver/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function listen() {
}
}
console.error(errors)
throw new Error('无法启动消息服务器')
throw new Error('无法启动接收器')
}

export const MESSAGE_SERVER_PORT = listen()
24 changes: 24 additions & 0 deletions src/main/typings/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,27 @@ export interface FileMessage extends BasicMessage {
}

export type Message = TextMessage | ImageMessage | FileMessage

export interface RtcBasicSignal {
from: number
to: number
timestamp: number
rtcId: string
}

export interface RtcOfferSignal extends RtcBasicSignal {
type: 'offer'
offer: RTCSessionDescriptionInit
}

export interface RtcAnswerSignal extends RtcBasicSignal {
type: 'answer'
answer: RTCSessionDescriptionInit
}

export interface RtcIceCandidateSignal extends RtcBasicSignal {
type: 'ice-candidate'
candidate: RTCIceCandidateInit
}

export type RtcSignal = RtcOfferSignal | RtcAnswerSignal | RtcIceCandidateSignal
14 changes: 14 additions & 0 deletions src/renderer/api/rtc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { RtcSignal } from '~/typings/app'

export async function postRtcSignal(
hostAndPort: string,
signal: RtcSignal,
) {
return await fetch(`http://${hostAndPort}/rtc/signal`, {
method: 'POST',
headers: { 'content-type': 'application/json; charset=UTF-8' },
body: JSON.stringify({
signal,
}),
})
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<script lang="ts" setup>
import { ElButton, ElMessage } from 'element-plus'
import { ElButton, ElMessage, ElTooltip } from 'element-plus'
import { type Ref, nextTick, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import MessageComponent from './Message.vue'
import type { Client } from '~/typings/app'
import type { Client, RtcActionSignal } from '~/typings/app'
import { useChatStore } from '~/stores/chat'
import { useAccountStore } from '~/stores/account'
import { useFriendStore } from '~/stores/friend'
import { useNetworkStore } from '~/stores/network'
import { useRtcStore } from '~/stores/rtc'
import { postFileMessage, postImageMessage, postTextMessage } from '~/api/message'
import { DISPLAY_MODE_ENABLE } from '~/config'
Expand All @@ -19,10 +20,12 @@ const fileInput = ref<HTMLInputElement>() as Ref<HTMLInputElement>
const router = useRouter()
const chatStore = useChatStore()
const rtcStore = useRtcStore()
const { selectedFriend, selectedMessages } = storeToRefs(chatStore)
const { uid } = storeToRefs(useAccountStore())
const { friendOnlineClients } = storeToRefs(useFriendStore())
const { networkInfo } = storeToRefs(useNetworkStore())
const { audioRtc } = storeToRefs(rtcStore)
const text = ref('')
const textMap = new Map<number, string>()
Expand Down Expand Up @@ -54,7 +57,7 @@ function getHostAndPort(client: Client) {
return `localhost:${client.port}`
}
function getFriendClient() {
function getSelectedFriendClient() {
const friendUid = selectedFriend.value?.uid
if (!friendUid)
throw new Error('好友 UID 为空值')
Expand All @@ -79,7 +82,7 @@ async function sendText() {
if (!uid.value)
throw new Error('当前 UID 为空值')
// 获取好友客户端信息
const client = getFriendClient()
const client = getSelectedFriendClient()
if (!client) {
ElMessage({ message: '好友不在线', type: 'warning', duration: 1500 })
return
Expand Down Expand Up @@ -110,7 +113,7 @@ async function sendImage(event: Event) {
if (!uid.value)
throw new Error('当前 UID 为空值')
// 获取好友客户端信息
const client = getFriendClient()
const client = getSelectedFriendClient()
if (!client) {
ElMessage({ message: '好友不在线', type: 'warning', duration: 1500 })
return
Expand Down Expand Up @@ -153,7 +156,7 @@ async function sendFile(event: Event) {
if (!uid.value)
throw new Error('当前 UID 为空值')
// 获取好友客户端信息
const client = getFriendClient()
const client = getSelectedFriendClient()
if (!client) {
ElMessage({ message: '好友不在线', type: 'warning', duration: 1500 })
return
Expand Down Expand Up @@ -185,6 +188,47 @@ async function sendFile(event: Event) {
})
fileInput.value.value = ''
}
async function sendRtcSignal(
options:
| { type: 'offer' }
| { type: 'answer' }
| { type: 'action'; action: RtcActionSignal['action'] },
) {
// 检查
if (!uid.value)
throw new Error('当前 UID 为空值')
// 获取相关信息
const client = getSelectedFriendClient()
if (!client) {
ElMessage({ message: '好友不在线', type: 'warning', duration: 1500 })
return
}
const hostAndPort = getHostAndPort(client)
if (!hostAndPort) {
ElMessage({ message: '无法进行 P2P 通信', type: 'warning', duration: 1500 })
return
}
switch (options.type) {
case 'offer':
rtcStore.postOffer(client.uid, hostAndPort)
break
case 'answer':
rtcStore.postAnswer(client.uid, hostAndPort)
break
case 'action':
rtcStore.postAction(client.uid, hostAndPort, options.action)
// 处理本地状态
switch (options.action) {
case 'cancel':
case 'reject':
case 'close':
rtcStore.closeAudioRtc(client.uid)
break
}
break
}
}
</script>

<template>
Expand All @@ -196,9 +240,13 @@ async function sendFile(event: Event) {
</div>
<div flex-grow flex flex-row-reverse items-center>
<button
i-carbon-user-avatar text-lg i-carbon-image opacity="65 hover:85" transition
i-carbon-user-avatar text-lg opacity="65 hover:85" transition
@click="router.replace(`/main/chat/friend_detail/${selectedFriend?.uid}`)"
/>
<button
i-carbon-cloud-logging text-lg opacity="65 hover:85" transition mr-2
@click="router.replace(`/main/chat/chat_log/${selectedFriend?.uid}`)"
/>
</div>
</div>
<div
Expand All @@ -212,7 +260,85 @@ async function sendFile(event: Event) {
/>
</div>
<div border-t-1 flex flex-col>
<div px-4 pt-2 space-x-2>
<div
v-if="audioRtc[selectedFriend.uid]?.status === 'connected'"
px-4 py-1 space-x-2 flex border-b-1 bg-green-5 text-white
>
<div>
语音通话中...
</div>
<div flex-grow flex flex-row-reverse items-center>
<ElTooltip content="挂断" placement="top">
<button
i-carbon-phone-off text-sm text-white
@click="sendRtcSignal({ type: 'action', action: 'close' })"
/>
</ElTooltip>
<ElTooltip
v-if="!audioRtc[selectedFriend.uid]?.muted"
content="静音" placement="top"
>
<button
i-carbon-volume-mute text-white mr-2
@click="() => {
if (selectedFriend?.uid)
rtcStore.muteAudioRtc(selectedFriend.uid)
}"
/>
</ElTooltip>
<ElTooltip
v-if="audioRtc[selectedFriend.uid]?.muted"
content="取消静音" placement="top"
>
<button
i-carbon-volume-up text-white mr-2
@click="() => {
if (selectedFriend?.uid)
rtcStore.unmuteAudioRtc(selectedFriend.uid)
}"
/>
</ElTooltip>
</div>
</div>
<div
v-if="audioRtc[selectedFriend.uid]?.status === 'requested'"
px-4 py-1 space-x-2 flex border-b-1 bg-sky-5 text-white
>
<div>
对方请求语音通话,是否接受?
</div>
<div flex-grow flex flex-row-reverse items-center>
<ElTooltip content="拒绝" placement="top">
<button
i-carbon-close text-sm text-white
@click="sendRtcSignal({ type: 'action', action: 'reject' })"
/>
</ElTooltip>
<ElTooltip content="接受" placement="top">
<button
i-carbon-checkmark text-sm text-white mr-2
@click="sendRtcSignal({ type: 'answer' })"
/>
</ElTooltip>
</div>
</div>
<div
v-if="audioRtc[selectedFriend.uid]?.status === 'waiting'"
px-4 py-1 space-x-2 flex border-b-1 bg-sky-5 text-white
>
<div>
正在等待对方接受语音通话...
</div>
<div flex-grow flex flex-row-reverse items-center>
<ElTooltip content="取消" placement="top">
<button
i-carbon-close text-sm text-white
@click="sendRtcSignal({ type: 'action', action: 'cancel' })"
/>
</ElTooltip>
</div>
</div>
<div px-4 pt-2 space-x-2 flex>
<button
i-carbon-image opacity="65 hover:85" transition
@click="imageInput.click()"
Expand All @@ -235,6 +361,10 @@ async function sendFile(event: Event) {
@change="sendFile"
>
</button>
<button
i-carbon-phone opacity="65 hover:85" transition
@click="sendRtcSignal({ type: 'offer' })"
/>
</div>
<div px-4 py-2 flex-grow overflow-hidden>
<textarea
Expand Down
Loading

0 comments on commit e822a72

Please sign in to comment.