<template>
<div class="chat-app">
<!-- 登录界面 -->
<div v-if="!currentUser" class="login-container">
<div class="login-box">
<h1>📱 WebSocket 即时通信</h1>
<div class="input-group">
<input
v-model="username"
type="text"
placeholder="请输入用户名"
@keyup.enter="login"
:disabled="isConnecting"
/>
<button @click="login" :disabled="!username.trim() || isConnecting">
{{ isConnecting ? '连接中...' : '登录' }}
</button>
</div>
<div class="user-list" v-if="onlineUsers.length > 0">
<h3>在线用户:</h3>
<div class="user-tags">
<span v-for="user in onlineUsers" :key="user.id" class="user-tag">
{{ user.username }}
</span>
</div>
</div>
</div>
</div>
<!-- 主聊天界面 -->
<div v-else class="main-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="user-info">
<div class="current-user">
<span class="user-avatar">{{ currentUser.username.charAt(0) }}</span>
<div>
<h3>{{ currentUser.username }}</h3>
<span class="user-status online">在线</span>
</div>
</div>
<button @click="logout" class="logout-btn">退出</button>
</div>
<div class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="搜索用户..."
/>
</div>
<div class="chat-list">
<h4>在线用户</h4>
<div
v-for="user in filteredUsers"
:key="user.id"
class="chat-item"
:class="{ active: activeChat?.id === user.id }"
@click="selectChat(user)"
>
<div class="chat-avatar">{{ user.username.charAt(0) }}</div>
<div class="chat-info">
<div class="chat-name">{{ user.username }}</div>
<div class="chat-last-msg" v-if="getLastMessage(user.id)">
{{ getLastMessage(user.id).content }}
</div>
</div>
<div class="chat-status" :class="{ online: user.online }"></div>
<div class="unread-count" v-if="getUnreadCount(user.id)">
{{ getUnreadCount(user.id) }}
</div>
</div>
</div>
<div class="connection-status">
<div class="status-indicator" :class="connectionStatus"></div>
<span>{{ connectionStatusText }}</span>
<button
v-if="connectionStatus === 'disconnected'"
@click="reconnect"
class="reconnect-btn"
>
重连
</button>
</div>
</div>
<!-- 聊天区域 -->
<div class="chat-area">
<div v-if="activeChat" class="chat-header">
<div class="chat-header-info">
<div class="chat-avatar">{{ activeChat.username.charAt(0) }}</div>
<div>
<h3>{{ activeChat.username }}</h3>
<span class="chat-status-text">
{{ activeChat.online ? '在线' : '离线' }}
</span>
</div>
</div>
<div class="chat-actions">
<button @click="clearChat" title="清空聊天记录">🗑️</button>
</div>
</div>
<div v-else class="no-chat-selected">
<div class="welcome-message">
<h2>👋 欢迎使用即时通信</h2>
<p>从左侧选择用户开始聊天</p>
</div>
</div>
<!-- 消息列表 -->
<div v-if="activeChat" class="messages-container" ref="messagesContainer">
<div
v-for="message in getChatMessages(activeChat.id)"
:key="message.id"
class="message"
:class="{
'message-sent': message.senderId === currentUser.id,
'message-received': message.senderId !== currentUser.id
}"
>
<div class="message-content">
<div class="message-text">{{ message.content }}</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
<span v-if="message.status === 'sending'">🕐</span>
<span v-if="message.status === 'sent'">✓</span>
<span v-if="message.status === 'delivered'">✓✓</span>
<span v-if="message.status === 'failed'" class="failed">✗</span>
</div>
</div>
</div>
</div>
<!-- 消息输入框 -->
<div v-if="activeChat" class="message-input">
<textarea
v-model="newMessage"
@keydown.enter.exact.prevent="sendMessage"
placeholder="输入消息... (按Enter发送,Shift+Enter换行)"
:disabled="!isConnected"
rows="2"
></textarea>
<button
@click="sendMessage"
:disabled="!newMessage.trim() || !isConnected"
class="send-btn"
>
{{ isConnected ? '发送' : '连接中...' }}
</button>
</div>
</div>
</div>
<!-- 连接状态提示 -->
<transition name="fade">
<div v-if="showConnectionAlert" class="connection-alert" :class="connectionStatus">
{{ connectionAlertText }}
</div>
</transition>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { v4 as uuidv4 } from 'uuid'
// 用户相关状态
const username = ref('')
const currentUser = ref(null)
const onlineUsers = ref([])
// WebSocket 状态
const socket = ref(null)
const isConnected = ref(false)
const isConnecting = ref(false)
const reconnectAttempts = ref(0)
const maxReconnectAttempts = 5
const reconnectTimer = ref(null)
// 聊天相关状态
const activeChat = ref(null)
const newMessage = ref('')
const searchKeyword = ref('')
const messagesContainer = ref(null)
// 聊天记录存储
const chatMessages = reactive({})
const unreadMessages = reactive({})
// 连接状态提示
const showConnectionAlert = ref(false)
const connectionAlertText = ref('')
// 用户ID映射(模拟用户ID生成)
const generateUserId = () => uuidv4()
// 连接状态
const connectionStatus = computed(() => {
if (!socket.value) return 'disconnected'
if (socket.value.readyState === WebSocket.OPEN) return 'connected'
if (socket.value.readyState === WebSocket.CONNECTING) return 'connecting'
return 'disconnected'
})
const connectionStatusText = computed(() => {
switch (connectionStatus.value) {
case 'connected': return '已连接'
case 'connecting': return '连接中...'
case 'disconnected': return '已断开'
default: return '未知状态'
}
})
// 过滤用户列表
const filteredUsers = computed(() => {
return onlineUsers.value.filter(user =>
user.id !== currentUser.value?.id &&
user.username.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
})
// 初始化 WebSocket 连接
const initWebSocket = () => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.close()
}
isConnecting.value = true
// 在实际应用中,这里应该是你的 WebSocket 服务器地址
// const wsUrl = 'ws://your-server.com/ws'
const wsUrl = 'wss://echo.websocket.org' // 使用公共测试服务器
try {
socket.value = new WebSocket(wsUrl)
socket.value.onopen = handleOpen
socket.value.onmessage = handleMessage
socket.value.onerror = handleError
socket.value.onclose = handleClose
// 设置连接超时
setTimeout(() => {
if (socket.value?.readyState === WebSocket.CONNECTING) {
showAlert('连接超时,请重试', 'error')
socket.value.close()
isConnecting.value = false
}
}, 10000)
} catch (error) {
console.error('WebSocket 初始化失败:', error)
showAlert('连接失败,请检查网络', 'error')
isConnecting.value = false
scheduleReconnect()
}
}
// WebSocket 事件处理
const handleOpen = () => {
console.log('WebSocket 连接已建立')
isConnected.value = true
isConnecting.value = false
reconnectAttempts.value = 0
showAlert('连接成功', 'success')
// 发送登录消息
if (currentUser.value) {
send({
type: 'login',
user: currentUser.value
})
}
}
const handleMessage = (event) => {
try {
const data = JSON.parse(event.data)
console.log('收到消息:', data)
switch (data.type) {
case 'login_success':
handleLoginSuccess(data)
break
case 'user_list':
handleUserList(data)
break
case 'message':
handleIncomingMessage(data)
break
case 'message_ack':
handleMessageAck(data)
break
case 'user_status':
handleUserStatus(data)
break
case 'ping':
send({ type: 'pong' })
break
default:
console.warn('未知消息类型:', data.type)
}
} catch (error) {
console.error('消息解析失败:', error)
}
}
const handleError = (error) => {
console.error('WebSocket 错误:', error)
showAlert('连接出现错误', 'error')
isConnected.value = false
}
const handleClose = (event) => {
console.log('WebSocket 连接关闭:', event.code, event.reason)
isConnected.value = false
if (!event.wasClean) {
showAlert('连接已断开,正在尝试重连...', 'warning')
scheduleReconnect()
}
}
// 消息处理函数
const handleLoginSuccess = (data) => {
currentUser.value = data.user
onlineUsers.value = data.users || []
showAlert('登录成功', 'success')
}
const handleUserList = (data) => {
onlineUsers.value = data.users.filter(user => user.id !== currentUser.value?.id)
}
const handleIncomingMessage = (data) => {
const { senderId, content, messageId, timestamp } = data
// 添加到聊天记录
if (!chatMessages[senderId]) {
chatMessages[senderId] = []
}
const message = {
id: messageId,
senderId,
content,
timestamp: timestamp || Date.now(),
status: 'delivered'
}
chatMessages[senderId].push(message)
// 更新未读消息数
if (activeChat.value?.id !== senderId) {
if (!unreadMessages[senderId]) {
unreadMessages[senderId] = 0
}
unreadMessages[senderId]++
}
// 发送消息回执
send({
type: 'message_ack',
messageId,
receiverId: senderId
})
// 自动滚动到底部
scrollToBottom()
}
const handleMessageAck = (data) => {
const { messageId, receiverId } = data
const messages = chatMessages[receiverId]
if (messages) {
const message = messages.find(msg => msg.id === messageId)
if (message) {
message.status = 'delivered'
}
}
}
const handleUserStatus = (data) => {
const { userId, online } = data
const userIndex = onlineUsers.value.findIndex(user => user.id === userId)
if (userIndex !== -1) {
onlineUsers.value[userIndex].online = online
}
}
// 发送消息
const send = (data) => {
if (socket.value?.readyState === WebSocket.OPEN) {
try {
socket.value.send(JSON.stringify(data))
} catch (error) {
console.error('发送消息失败:', error)
showAlert('发送失败,请检查连接', 'error')
}
} else {
console.warn('WebSocket 未连接,无法发送消息')
showAlert('连接已断开,请重新连接', 'error')
}
}
// 登录
const login = () => {
if (!username.value.trim()) {
showAlert('请输入用户名', 'error')
return
}
const user = {
id: generateUserId(),
username: username.value.trim(),
online: true
}
currentUser.value = user
// 初始化用户的聊天记录存储
chatMessages[user.id] = []
initWebSocket()
}
// 登出
const logout = () => {
if (socket.value) {
send({ type: 'logout', userId: currentUser.value.id })
socket.value.close()
}
currentUser.value = null
username.value = ''
activeChat.value = null
Object.keys(chatMessages).forEach(key => delete chatMessages[key])
Object.keys(unreadMessages).forEach(key => delete unreadMessages[key])
}
// 选择聊天
const selectChat = (user) => {
activeChat.value = user
// 清空该用户的未读消息
if (unreadMessages[user.id]) {
unreadMessages[user.id] = 0
}
// 确保聊天记录存在
if (!chatMessages[user.id]) {
chatMessages[user.id] = []
}
// 滚动到底部
nextTick(() => scrollToBottom())
}
// 发送消息
const sendMessage = () => {
if (!newMessage.value.trim() || !activeChat.value || !isConnected.value) return
const messageId = uuidv4()
const timestamp = Date.now()
const message = {
id: messageId,
senderId: currentUser.value.id,
content: newMessage.value.trim(),
timestamp,
status: 'sending'
}
// 添加到本地聊天记录
if (!chatMessages[activeChat.value.id]) {
chatMessages[activeChat.value.id] = []
}
chatMessages[activeChat.value.id].push(message)
// 发送到服务器
send({
type: 'message',
messageId,
receiverId: activeChat.value.id,
content: newMessage.value.trim(),
timestamp
})
// 清空输入框
newMessage.value = ''
// 滚动到底部
nextTick(() => scrollToBottom())
// 模拟消息发送状态更新(在实际应用中由服务器回执控制)
setTimeout(() => {
const messages = chatMessages[activeChat.value.id]
const sentMessage = messages.find(msg => msg.id === messageId)
if (sentMessage) {
sentMessage.status = isConnected.value ? 'sent' : 'failed'
}
}, 1000)
}
// 清空聊天
const clearChat = () => {
if (activeChat.value && chatMessages[activeChat.value.id]) {
if (confirm('确定要清空与 ' + activeChat.value.username + ' 的聊天记录吗?')) {
chatMessages[activeChat.value.id] = []
}
}
}
// 重连机制
const scheduleReconnect = () => {
if (reconnectAttempts.value >= maxReconnectAttempts) {
showAlert('重连次数超限,请刷新页面', 'error')
return
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
}
reconnectAttempts.value++
const delay = Math.min(3000 * reconnectAttempts.value, 30000) // 指数退避
reconnectTimer.value = setTimeout(() => {
if (currentUser.value) {
console.log(`尝试第 ${reconnectAttempts.value} 次重连...`)
initWebSocket()
}
}, delay)
}
const reconnect = () => {
if (currentUser.value) {
reconnectAttempts.value = 0
initWebSocket()
}
}
// 工具函数
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})
}
const getChatMessages = (userId) => {
return chatMessages[userId] || []
}
const getLastMessage = (userId) => {
const messages = chatMessages[userId]
return messages && messages.length > 0 ? messages[messages.length - 1] : null
}
const getUnreadCount = (userId) => {
return unreadMessages[userId] || 0
}
const showAlert = (text, type = 'info') => {
connectionAlertText.value = text
showConnectionAlert.value = true
setTimeout(() => {
showConnectionAlert.value = false
}, 3000)
}
// 心跳检测
let heartbeatInterval = null
const startHeartbeat = () => {
heartbeatInterval = setInterval(() => {
if (socket.value?.readyState === WebSocket.OPEN) {
send({ type: 'ping', timestamp: Date.now() })
}
}, 30000) // 30秒一次心跳
}
// 生命周期钩子
onMounted(() => {
// 恢复上次登录的用户(如果有)
const savedUser = localStorage.getItem('ws_chat_user')
if (savedUser) {
try {
const user = JSON.parse(savedUser)
username.value = user.username
} catch (e) {
localStorage.removeItem('ws_chat_user')
}
}
})
onUnmounted(() => {
// 清理
if (socket.value) {
socket.value.close()
}
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
}
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
}
// 保存当前用户
if (currentUser.value) {
localStorage.setItem('ws_chat_user', JSON.stringify({
username: currentUser.value.username
}))
}
})
// 监听连接状态变化
watch(connectionStatus, (status) => {
if (status === 'connected') {
startHeartbeat()
} else if (heartbeatInterval) {
clearInterval(heartbeatInterval)
heartbeatInterval = null
}
})
</script>
<style scoped>
.chat-app {
width: 100%;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
width: 100%;
max-width: 400px;
}
.login-box {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
}
.login-box h1 {
margin-bottom: 30px;
color: #333;
font-size: 24px;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.input-group input:focus {
border-color: #667eea;
}
.input-group input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.input-group button {
padding: 12px 25px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.3s;
}
.input-group button:hover:not(:disabled) {
transform: translateY(-2px);
}
.input-group button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.user-list {
margin-top: 20px;
text-align: left;
}
.user-list h3 {
color: #666;
margin-bottom: 10px;
font-size: 14px;
}
.user-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.user-tag {
background: #f0f0f0;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
color: #666;
}
.main-container {
width: 100%;
height: 90vh;
max-width: 1200px;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
display: flex;
}
.sidebar {
width: 300px;
background: #f8f9fa;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.user-info {
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.current-user {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.current-user h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.user-status {
font-size: 12px;
color: #666;
}
.user-status.online {
color: #4caf50;
}
.logout-btn {
padding: 6px 12px;
background: #ff4757;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: opacity 0.3s;
}
.logout-btn:hover {
opacity: 0.9;
}
.search-box {
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
}
.search-box input {
width: 100%;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
outline: none;
}
.search-box input:focus {
border-color: #667eea;
}
.chat-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.chat-list h4 {
margin: 0 0 15px 0;
color: #666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.chat-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.3s;
position: relative;
margin-bottom: 8px;
}
.chat-item:hover {
background: #e9ecef;
}
.chat-item.active {
background: #e3f2fd;
}
.chat-avatar {
width: 36px;
height: 36px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 12px;
}
.chat-info {
flex: 1;
min-width: 0;
}
.chat-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-last-msg {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-status {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ddd;
margin-left: 10px;
}
.chat-status.online {
background: #4caf50;
}
.unread-count {
position: absolute;
right: 12px;
top: 12px;
background: #ff4757;
color: white;
font-size: 11px;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.connection-status {
padding: 15px 20px;
border-top: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
color: #666;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-indicator.connected {
background: #4caf50;
animation: pulse 2s infinite;
}
.status-indicator.connecting {
background: #ff9800;
}
.status-indicator.disconnected {
background: #ff4757;
}
.reconnect-btn {
margin-left: auto;
padding: 4px 8px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header-info {
display: flex;
align-items: center;
gap: 12px;
}
.chat-status-text {
font-size: 12px;
color: #666;
}
.chat-actions button {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.3s;
}
.chat-actions button:hover {
opacity: 1;
}
.no-chat-selected {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #fafafa;
}
.welcome-message {
text-align: center;
color: #666;
}
.welcome-message h2 {
margin-bottom: 10px;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #fafafa;
display: flex;
flex-direction: column;
gap: 10px;
}
.message {
max-width: 70%;
animation: messageAppear 0.3s ease-out;
}
@keyframes messageAppear {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-sent {
align-self: flex-end;
}
.message-received {
align-self: flex-start;
}
.message-content {
padding: 12px 16px;
border-radius: 18px;
position: relative;
}
.message-sent .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message-received .message-content {
background: white;
color: #333;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.message-text {
word-break: break-word;
line-height: 1.4;
}
.message-time {
font-size: 11px;
opacity: 0.8;
margin-top: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.message-sent .message-time {
justify-content: flex-end;
}
.failed {
color: #ff4757;
}
.message-input {
padding: 20px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
align-items: flex-end;
}
.message-input textarea {
flex: 1;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
outline: none;
resize: none;
font-family: inherit;
transition: border-color 0.3s;
}
.message-input textarea:focus {
border-color: #667eea;
}
.message-input textarea:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.send-btn {
padding: 12px 25px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.3s;
height: 44px;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-2px);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.connection-alert {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 24px;
border-radius: 10px;
color: white;
font-weight: 500;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.connection-alert.connected {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
}
.connection-alert.error {
background: linear-gradient(135deg, #ff4757 0%, #c44569 100%);
}
.connection-alert.warning {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(76, 175, 80, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
}
}
@media (max-width: 768px) {
.main-container {
height: 100vh;
border-radius: 0;
}
.sidebar {
width: 100%;
}
.message {
max-width: 85%;
}
}
</style>