2025-12-19 15:11:49 +08:00
|
|
|
|
package com.example.livestreaming;
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import android.content.ClipData;
|
|
|
|
|
|
import android.content.ClipboardManager;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.content.Context;
|
|
|
|
|
|
import android.content.Intent;
|
|
|
|
|
|
import android.os.Bundle;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import android.os.Handler;
|
|
|
|
|
|
import android.os.Looper;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
|
import android.view.KeyEvent;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import android.view.MenuItem;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.view.View;
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.annotation.Nullable;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import androidx.appcompat.app.AlertDialog;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import androidx.appcompat.app.AppCompatActivity;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import androidx.appcompat.widget.PopupMenu;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
|
|
|
|
|
|
|
|
|
|
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import com.google.android.material.snackbar.Snackbar;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
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<ChatMessage> messages = new ArrayList<>();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
private Handler handler;
|
|
|
|
|
|
private Runnable statusUpdateRunnable;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-22 16:31:46 +08:00
|
|
|
|
private static final String EXTRA_UNREAD_COUNT = "extra_unread_count";
|
|
|
|
|
|
|
|
|
|
|
|
private int initialUnreadCount = 0;
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
|
|
|
|
|
super.onCreate(savedInstanceState);
|
|
|
|
|
|
binding = ActivityConversationBinding.inflate(getLayoutInflater());
|
|
|
|
|
|
setContentView(binding.getRoot());
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
handler = new Handler(Looper.getMainLooper());
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
|
|
|
|
|
|
binding.titleText.setText(title != null ? title : "会话");
|
2025-12-22 16:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取该会话的初始未读数量
|
|
|
|
|
|
initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
binding.backButton.setOnClickListener(v -> finish());
|
|
|
|
|
|
|
|
|
|
|
|
setupMessages();
|
|
|
|
|
|
setupInput();
|
2025-12-22 16:31:46 +08:00
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// TODO: 接入后端接口 - 标记会话消息为已读
|
|
|
|
|
|
// 接口路径: POST /api/conversations/{conversationId}/read
|
|
|
|
|
|
// 请求参数:
|
|
|
|
|
|
// - conversationId: 会话ID(路径参数)
|
|
|
|
|
|
// - userId: 当前用户ID(从token中获取)
|
|
|
|
|
|
// 返回数据格式: ApiResponse<{success: boolean}>
|
|
|
|
|
|
// 用户进入会话时,标记该会话的所有消息为已读,减少未读数量
|
2025-12-22 16:31:46 +08:00
|
|
|
|
if (initialUnreadCount > 0) {
|
|
|
|
|
|
UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected void onPause() {
|
|
|
|
|
|
super.onPause();
|
|
|
|
|
|
// 当用户离开会话时,更新总未读数量(如果会话未读数量已减少)
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void setupMessages() {
|
|
|
|
|
|
adapter = new ConversationMessagesAdapter();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置长按监听
|
|
|
|
|
|
adapter.setOnMessageLongClickListener((message, position, view) -> {
|
|
|
|
|
|
showMessageMenu(message, position, view);
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
|
|
|
|
|
layoutManager.setStackFromEnd(false);
|
|
|
|
|
|
binding.messagesRecyclerView.setLayoutManager(layoutManager);
|
|
|
|
|
|
binding.messagesRecyclerView.setAdapter(adapter);
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// TODO: 接入后端接口 - 获取会话消息列表
|
|
|
|
|
|
// 接口路径: GET /api/conversations/{conversationId}/messages
|
|
|
|
|
|
// 请求参数:
|
|
|
|
|
|
// - conversationId: 会话ID(路径参数)
|
|
|
|
|
|
// - page (可选): 页码,用于分页加载历史消息
|
|
|
|
|
|
// - pageSize (可选): 每页数量,默认20
|
|
|
|
|
|
// - beforeMessageId (可选): 获取指定消息ID之前的消息,用于上拉加载更多
|
|
|
|
|
|
// 返回数据格式: ApiResponse<List<ChatMessage>>
|
|
|
|
|
|
// ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
|
|
|
|
|
|
// 消息列表应按时间正序排列(最早的在前面)
|
2025-12-19 15:11:49 +08:00
|
|
|
|
messages.clear();
|
|
|
|
|
|
String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : "";
|
2025-12-23 15:37:37 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
adapter.submitList(new ArrayList<>(messages));
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
}
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
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() {
|
2025-12-23 18:09:56 +08:00
|
|
|
|
// 检查登录状态,发送私信需要登录
|
|
|
|
|
|
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
// TODO: 接入后端接口 - 发送私信消息
|
|
|
|
|
|
// 接口路径: POST /api/conversations/{conversationId}/messages
|
|
|
|
|
|
// 请求参数:
|
|
|
|
|
|
// - conversationId: 会话ID(路径参数)
|
|
|
|
|
|
// - message: 消息内容
|
|
|
|
|
|
// - userId: 发送者用户ID(从token中获取)
|
|
|
|
|
|
// 返回数据格式: ApiResponse<ChatMessage>
|
|
|
|
|
|
// 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"}>
|
2025-12-19 15:11:49 +08:00
|
|
|
|
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
|
|
|
|
|
|
if (TextUtils.isEmpty(text)) return;
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
ChatMessage newMessage = new ChatMessage("我", text);
|
|
|
|
|
|
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
|
|
|
|
|
messages.add(newMessage);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
adapter.submitList(new ArrayList<>(messages));
|
|
|
|
|
|
binding.messageInput.setText("");
|
|
|
|
|
|
scrollToBottom();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 模拟消息发送过程:发送中 -> 已发送 -> 已读
|
|
|
|
|
|
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);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void scrollToBottom() {
|
|
|
|
|
|
if (messages.isEmpty()) return;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
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;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|