package com.example.livestreaming; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.PopupMenu; import androidx.recyclerview.widget.LinearLayoutManager; import com.example.livestreaming.databinding.ActivityConversationBinding; import com.google.android.material.snackbar.Snackbar; import java.util.ArrayList; import java.util.List; public class ConversationActivity extends AppCompatActivity { private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id"; private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title"; private ActivityConversationBinding binding; private ConversationMessagesAdapter adapter; private final List messages = new ArrayList<>(); private Handler handler; private Runnable statusUpdateRunnable; public static void start(Context context, String conversationId, String title) { Intent intent = new Intent(context, ConversationActivity.class); intent.putExtra(EXTRA_CONVERSATION_ID, conversationId); intent.putExtra(EXTRA_CONVERSATION_TITLE, title); context.startActivity(intent); } private static final String EXTRA_UNREAD_COUNT = "extra_unread_count"; private int initialUnreadCount = 0; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityConversationBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); handler = new Handler(Looper.getMainLooper()); String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null; binding.titleText.setText(title != null ? title : "会话"); // 获取该会话的初始未读数量 initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0; binding.backButton.setOnClickListener(v -> finish()); setupMessages(); setupInput(); // TODO: 接入后端接口 - 标记会话消息为已读 // 接口路径: POST /api/conversations/{conversationId}/read // 请求参数: // - conversationId: 会话ID(路径参数) // - userId: 当前用户ID(从token中获取) // 返回数据格式: ApiResponse<{success: boolean}> // 用户进入会话时,标记该会话的所有消息为已读,减少未读数量 if (initialUnreadCount > 0) { UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount); } } @Override protected void onPause() { super.onPause(); // 当用户离开会话时,更新总未读数量(如果会话未读数量已减少) } private void setupMessages() { adapter = new ConversationMessagesAdapter(); // 设置长按监听 adapter.setOnMessageLongClickListener((message, position, view) -> { showMessageMenu(message, position, view); }); LinearLayoutManager layoutManager = new LinearLayoutManager(this); layoutManager.setStackFromEnd(false); binding.messagesRecyclerView.setLayoutManager(layoutManager); binding.messagesRecyclerView.setAdapter(adapter); // TODO: 接入后端接口 - 获取会话消息列表 // 接口路径: GET /api/conversations/{conversationId}/messages // 请求参数: // - conversationId: 会话ID(路径参数) // - page (可选): 页码,用于分页加载历史消息 // - pageSize (可选): 每页数量,默认20 // - beforeMessageId (可选): 获取指定消息ID之前的消息,用于上拉加载更多 // 返回数据格式: ApiResponse> // ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段 // 消息列表应按时间正序排列(最早的在前面) messages.clear(); String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : ""; ChatMessage incomingMsg = new ChatMessage(title, "你好~"); incomingMsg.setStatus(ChatMessage.MessageStatus.SENT); messages.add(incomingMsg); ChatMessage outgoingMsg = new ChatMessage("我", "在的,有什么需要帮忙?"); outgoingMsg.setStatus(ChatMessage.MessageStatus.READ); messages.add(outgoingMsg); adapter.submitList(new ArrayList<>(messages)); scrollToBottom(); } private void showMessageMenu(ChatMessage message, int position, View anchorView) { PopupMenu popupMenu = new PopupMenu(this, anchorView); popupMenu.getMenu().add(0, 0, 0, "复制"); popupMenu.getMenu().add(0, 1, 0, "删除"); popupMenu.setOnMenuItemClickListener(item -> { if (item.getItemId() == 0) { // 复制消息 copyMessage(message); return true; } else if (item.getItemId() == 1) { // 删除消息 deleteMessage(position); return true; } return false; }); popupMenu.show(); } private void copyMessage(ChatMessage message) { ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("消息", message.getMessage()); clipboard.setPrimaryClip(clip); Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show(); } private void deleteMessage(int position) { // TODO: 接入后端接口 - 删除消息 // 接口路径: DELETE /api/messages/{messageId} // 请求参数: // - messageId: 消息ID(路径参数) // - userId: 当前用户ID(从token中获取,验证是否为消息发送者) // 返回数据格式: ApiResponse<{success: boolean}> // 删除成功后,从本地消息列表移除 if (position < 0 || position >= messages.size()) return; new AlertDialog.Builder(this) .setTitle("删除消息") .setMessage("确定要删除这条消息吗?") .setPositiveButton("删除", (dialog, which) -> { messages.remove(position); adapter.submitList(new ArrayList<>(messages)); Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show(); }) .setNegativeButton("取消", null) .show(); } private void setupInput() { binding.sendButton.setOnClickListener(v -> sendMessage()); binding.messageInput.setOnEditorActionListener((v, actionId, event) -> { if (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { sendMessage(); return true; } return false; }); binding.messageInput.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) scrollToBottom(); }); } private void sendMessage() { // 检查登录状态,发送私信需要登录 if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) { return; } // TODO: 接入后端接口 - 发送私信消息 // 接口路径: POST /api/conversations/{conversationId}/messages // 请求参数: // - conversationId: 会话ID(路径参数) // - message: 消息内容 // - userId: 发送者用户ID(从token中获取) // 返回数据格式: ApiResponse // ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段 // 发送成功后,更新消息状态为SENT,并添加到消息列表 // TODO: 接入后端接口 - 接收对方已读状态(WebSocket或轮询) // 方案1: WebSocket实时推送 // - 连接: ws://api.example.com/conversations/{conversationId} // - 接收消息格式: {type: "read", messageId: "xxx"} 或 {type: "new_message", data: ChatMessage} // 方案2: 轮询检查消息状态 // - 接口路径: GET /api/conversations/{conversationId}/messages/{messageId}/status // - 返回数据格式: ApiResponse<{status: "sent"|"read"}> String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : ""; if (TextUtils.isEmpty(text)) return; ChatMessage newMessage = new ChatMessage("我", text); newMessage.setStatus(ChatMessage.MessageStatus.SENDING); messages.add(newMessage); adapter.submitList(new ArrayList<>(messages)); binding.messageInput.setText(""); scrollToBottom(); // 模拟消息发送过程:发送中 -> 已发送 -> 已读 if (statusUpdateRunnable != null) { handler.removeCallbacks(statusUpdateRunnable); } statusUpdateRunnable = () -> { // 1秒后更新为已发送 newMessage.setStatus(ChatMessage.MessageStatus.SENT); adapter.notifyItemChanged(messages.size() - 1); // 再2秒后更新为已读 handler.postDelayed(() -> { newMessage.setStatus(ChatMessage.MessageStatus.READ); adapter.notifyItemChanged(messages.size() - 1); }, 2000); }; handler.postDelayed(statusUpdateRunnable, 1000); } private void scrollToBottom() { if (messages.isEmpty()) return; binding.messagesRecyclerView.post(() -> { int lastPosition = messages.size() - 1; if (lastPosition >= 0) { binding.messagesRecyclerView.smoothScrollToPosition(lastPosition); } }); } @Override protected void onDestroy() { super.onDestroy(); // 清理Handler中的延迟任务,防止内存泄漏 if (handler != null && statusUpdateRunnable != null) { handler.removeCallbacks(statusUpdateRunnable); } handler = null; statusUpdateRunnable = null; } }