zhibo/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
2025-12-26 15:16:40 +08:00

982 lines
39 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.util.Log;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
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 androidx.recyclerview.widget.RecyclerView;
import com.example.livestreaming.databinding.ActivityConversationBinding;
import com.example.livestreaming.net.AuthStore;
import com.google.android.material.snackbar.Snackbar;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ConversationActivity extends AppCompatActivity {
private static final String TAG = "ConversationActivity";
private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id";
private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title";
private static final String EXTRA_UNREAD_COUNT = "extra_unread_count";
private static final int PAGE_SIZE = 50; // 增加每页消息数量
private static final long POLL_INTERVAL = 3000; // 3秒轮询一次
private ActivityConversationBinding binding;
private final OkHttpClient httpClient = new OkHttpClient();
private ConversationMessagesAdapter adapter;
private final List<ChatMessage> messages = new ArrayList<>();
private Handler handler;
private Runnable pollRunnable;
private boolean isPolling = false;
private boolean isUserScrolling = false; // 用户是否正在滚动查看历史记录
private String lastMessageId = null; // 记录最后一条消息ID用于判断是否有新消息
private boolean isLoadingMore = false; // 是否正在加载更多
private boolean hasMoreHistory = true; // 是否还有更多历史消息
private int historyPage = 1; // 历史消息页码
private String conversationId;
private int currentPage = 1;
private int initialUnreadCount = 0;
private String currentUserId;
// 回复消息相关
private ChatMessage replyToMessage = null;
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);
}
@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 : "会话");
conversationId = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_ID) : null;
initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0;
// 先尝试从 AuthStore 获取 userId
currentUserId = AuthStore.getUserId(this);
Log.d(TAG, "从AuthStore获取的用户ID: " + currentUserId);
// 如果 userId 为空,尝试从用户信息接口获取
if (currentUserId == null || currentUserId.isEmpty()) {
fetchCurrentUserId();
}
binding.backButton.setOnClickListener(new DebounceClickListener() {
@Override
public void onDebouncedClick(View v) {
finish();
}
});
// 确保在加载消息前获取到正确的userId
ensureUserIdLoaded();
setupMessages();
setupInput();
setupReplyInput();
// 标记会话为已读
if (initialUnreadCount > 0 && conversationId != null) {
markConversationAsRead();
}
}
@Override
protected void onPause() {
super.onPause();
stopPolling();
}
@Override
protected void onResume() {
super.onResume();
startPolling();
}
/**
* 开始轮询新消息
*/
private void startPolling() {
if (isPolling) return;
isPolling = true;
pollRunnable = new Runnable() {
@Override
public void run() {
if (!isPolling || isFinishing() || isDestroyed()) return;
loadMessagesFromServer();
handler.postDelayed(this, POLL_INTERVAL);
}
};
// 延迟开始轮询,避免和初始加载冲突
handler.postDelayed(pollRunnable, POLL_INTERVAL);
}
/**
* 停止轮询
*/
private void stopPolling() {
isPolling = false;
if (pollRunnable != null && handler != null) {
handler.removeCallbacks(pollRunnable);
}
}
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);
// 监听滚动状态,判断用户是否在查看历史记录,以及加载更多历史消息
binding.messagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
// 用户开始拖动滚动,标记为正在滚动
isUserScrolling = true;
Log.d(TAG, "用户开始滚动查看历史记录");
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 检查是否滚动到底部
if (!recyclerView.canScrollVertically(1)) {
// 已经滚动到底部,重置标志
isUserScrolling = false;
Log.d(TAG, "用户已滚动到底部,恢复自动滚动");
}
// 检查是否滚动到顶部,加载更多历史消息
if (!recyclerView.canScrollVertically(-1) && dy < 0) {
// 滚动到顶部且向上滚动,加载更多历史消息
loadMoreHistory();
}
}
});
loadMessagesFromServer();
}
/**
* 确保userId已加载如果没有则从服务器获取
*/
private void ensureUserIdLoaded() {
// 先尝试从 AuthStore 获取
currentUserId = AuthStore.getUserId(this);
Log.d(TAG, "ensureUserIdLoaded: 从AuthStore获取的userId = " + currentUserId);
// 如果为空,尝试从服务器获取
if (currentUserId == null || currentUserId.isEmpty()) {
fetchCurrentUserId();
}
}
/**
* 从服务器获取当前用户ID
*/
private void fetchCurrentUserId() {
String token = AuthStore.getToken(this);
if (token == null) {
Log.w(TAG, "未登录无法获取用户ID");
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/user/info";
Log.d(TAG, "获取用户信息: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "获取用户信息失败", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "用户信息响应: " + body);
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONObject data = json.optJSONObject("data");
if (data != null) {
int uid = data.optInt("uid", 0);
if (uid > 0) {
currentUserId = String.valueOf(uid);
// 保存到 AuthStore
AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", ""));
Log.d(TAG, "从服务器获取到用户ID: " + currentUserId);
// 重新加载消息以正确显示
runOnUiThread(() -> loadMessagesFromServer());
}
}
}
} catch (Exception e) {
Log.e(TAG, "解析用户信息失败", e);
}
}
});
}
/**
* 从服务器加载消息列表
*/
private void loadMessagesFromServer() {
String token = AuthStore.getToken(this);
if (token == null || conversationId == null) {
Log.w(TAG, "未登录或会话ID为空");
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages?page=" + currentPage + "&pageSize=" + PAGE_SIZE;
Log.d(TAG, "加载消息列表: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "加载消息列表失败", e);
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "加载消息失败", Snackbar.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "消息列表响应: " + body);
runOnUiThread(() -> parseMessages(body));
}
});
}
private void parseMessages(String body) {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONArray data = json.optJSONArray("data");
// 创建新消息列表
List<ChatMessage> newMessages = new ArrayList<>();
if (data != null) {
for (int i = 0; i < data.length(); i++) {
JSONObject item = data.getJSONObject(i);
ChatMessage msg = parseChatMessage(item);
if (msg != null) {
newMessages.add(msg);
}
}
}
// 消息按时间倒序返回,需要反转为正序显示
Collections.reverse(newMessages);
// 检查是否有新消息比较最后一条消息ID
boolean hasNewMessages = false;
String newLastId = null;
if (!newMessages.isEmpty()) {
newLastId = newMessages.get(newMessages.size() - 1).getMessageId();
if (lastMessageId == null || !lastMessageId.equals(newLastId)) {
hasNewMessages = true;
}
}
// 更新最后消息ID
if (newLastId != null) {
lastMessageId = newLastId;
}
// 更新消息列表
messages.clear();
messages.addAll(newMessages);
adapter.submitList(new ArrayList<>(messages));
// 只有在用户不滚动查看历史记录时,且有新消息时才滚动到底部
if (!isUserScrolling && hasNewMessages) {
Log.d(TAG, "有新消息,自动滚动到底部");
scrollToBottom();
} else if (isUserScrolling) {
Log.d(TAG, "用户正在查看历史记录,不自动滚动");
}
} else {
String msg = json.optString("message", "加载失败");
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析消息列表失败", e);
}
}
private ChatMessage parseChatMessage(JSONObject item) {
try {
String messageId = item.optString("messageId", "");
int senderId = item.optInt("userId", 0); // 后端返回的 userId 实际是 senderId
String username = item.optString("username", "未知用户");
String message = item.optString("message", "");
long timestamp = item.optLong("timestamp", System.currentTimeMillis());
String status = item.optString("status", "sent");
String avatarUrl = item.optString("avatarUrl", "");
boolean isSystem = item.optBoolean("isSystemMessage", false);
String messageType = item.optString("messageType", "text");
// 判断是否是自己发送的消息
// 使用成员变量 currentUserId确保整个会话期间使用同一个值
String myUserId = currentUserId;
if (myUserId == null || myUserId.isEmpty()) {
// 如果成员变量为空,尝试从 AuthStore 获取
myUserId = AuthStore.getUserId(this);
if (myUserId != null && !myUserId.isEmpty()) {
currentUserId = myUserId; // 同步更新成员变量
}
}
boolean isMine = false;
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
try {
// 修复:将 myUserId 转换为整数再比较,避免小数点问题(如 "43.0" vs 43
int myUserIdInt = (int) Double.parseDouble(myUserId);
isMine = myUserIdInt == senderId;
} catch (NumberFormatException e) {
Log.w(TAG, "无法解析userId: " + myUserId);
// 降级处理:直接字符串比较
isMine = myUserId.equals(String.valueOf(senderId));
}
}
Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine + ", messageId=" + messageId);
// 关键修复:使用 "我" 作为自己发送消息的用户名,确保显示在右侧
String displayName = isMine ? "" : username;
ChatMessage.MessageStatus msgStatus;
switch (status) {
case "sending":
msgStatus = ChatMessage.MessageStatus.SENDING;
break;
case "read":
msgStatus = ChatMessage.MessageStatus.READ;
break;
default:
msgStatus = ChatMessage.MessageStatus.SENT;
}
ChatMessage chatMessage = new ChatMessage(messageId, displayName, message, timestamp, isSystem, msgStatus);
chatMessage.setAvatarUrl(avatarUrl);
// 设置消息类型
if ("image".equals(messageType)) {
chatMessage.setMessageType(ChatMessage.MessageType.IMAGE);
} else if ("voice".equals(messageType)) {
chatMessage.setMessageType(ChatMessage.MessageType.VOICE);
} else {
chatMessage.setMessageType(ChatMessage.MessageType.TEXT);
}
// 保存原始的 senderId用于撤回功能判断
chatMessage.setSenderId(senderId);
// 解析引用消息信息
// 注意:后端返回的 replyToMessageId 可能是字符串或 null
String replyToMessageId = null;
if (!item.isNull("replyToMessageId")) {
replyToMessageId = item.optString("replyToMessageId", "");
if (replyToMessageId.isEmpty() || "null".equals(replyToMessageId)) {
replyToMessageId = null;
}
}
Log.d(TAG, "解析消息 " + messageId + " 的引用信息: replyToMessageId=" + replyToMessageId);
if (replyToMessageId != null) {
chatMessage.setReplyToMessageId(replyToMessageId);
String replyToContent = item.optString("replyToContent", "");
String replyToSenderName = item.optString("replyToSenderName", "");
int replyToSenderId = item.optInt("replyToSenderId", 0);
String replyToMessageType = item.optString("replyToMessageType", "text");
chatMessage.setReplyToContent(replyToContent);
chatMessage.setReplyToSenderId(replyToSenderId);
chatMessage.setReplyToSenderName(replyToSenderName);
chatMessage.setReplyToMessageType(replyToMessageType);
Log.d(TAG, "解析到引用消息: replyToId=" + replyToMessageId
+ ", replyToSender=" + replyToSenderName
+ ", replyToContent=" + replyToContent
+ ", replyToType=" + replyToMessageType);
}
return chatMessage;
} catch (Exception e) {
Log.e(TAG, "解析消息失败", e);
return null;
}
}
/**
* 标记会话为已读
*/
private void markConversationAsRead() {
String token = AuthStore.getToken(this);
if (token == null || conversationId == null) return;
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/read";
Log.d(TAG, "标记已读: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.post(RequestBody.create("", MediaType.parse("application/json")))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "标记已读失败", e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "标记已读响应: " + body);
runOnUiThread(() -> {
if (initialUnreadCount > 0) {
UnreadMessageManager.decrementUnreadCount(ConversationActivity.this, initialUnreadCount);
}
});
}
});
}
private void showMessageMenu(ChatMessage message, int position, View anchorView) {
PopupMenu popupMenu = new PopupMenu(this, anchorView);
popupMenu.getMenu().add(0, 0, 0, "复制");
// 已撤回的消息不能被回复
if (!"[消息已撤回]".equals(message.getMessage())) {
popupMenu.getMenu().add(0, 3, 0, "回复");
}
// 只有自己发送的消息才能删除和撤回
if ("".equals(message.getUsername())) {
popupMenu.getMenu().add(0, 1, 0, "删除");
// 检查是否在2分钟内可以撤回已撤回的消息不能再撤回
if (!"[消息已撤回]".equals(message.getMessage())) {
long now = System.currentTimeMillis();
long messageTime = message.getTimestamp();
long diffMinutes = (now - messageTime) / (1000 * 60);
if (diffMinutes <= 2) {
popupMenu.getMenu().add(0, 2, 0, "撤回");
}
}
}
popupMenu.setOnMenuItemClickListener(item -> {
if (item.getItemId() == 0) {
copyMessage(message);
return true;
} else if (item.getItemId() == 1) {
deleteMessage(message, position);
return true;
} else if (item.getItemId() == 2) {
recallMessage(message, position);
return true;
} else if (item.getItemId() == 3) {
showReplyPreview(message);
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(ChatMessage message, int position) {
if (position < 0 || position >= messages.size()) return;
new AlertDialog.Builder(this)
.setTitle("删除消息")
.setMessage("确定要删除这条消息吗?")
.setPositiveButton("删除", (dialog, which) -> deleteMessageFromServer(message, position))
.setNegativeButton("取消", null)
.show();
}
private void deleteMessageFromServer(ChatMessage message, int position) {
String token = AuthStore.getToken(this);
if (token == null) return;
String messageId = message.getMessageId();
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId;
Log.d(TAG, "删除消息: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.delete()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "删除消息失败", e);
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "删除失败", Snackbar.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "删除消息响应: " + body);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
messages.remove(position);
adapter.submitList(new ArrayList<>(messages));
Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show();
} else {
Snackbar.make(binding.getRoot(), json.optString("message", "删除失败"), Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析删除响应失败", e);
}
});
}
});
}
/**
* 撤回消息
*/
private void recallMessage(ChatMessage message, int position) {
if (position < 0 || position >= messages.size()) return;
new AlertDialog.Builder(this)
.setTitle("撤回消息")
.setMessage("确定要撤回这条消息吗?")
.setPositiveButton("撤回", (dialog, which) -> recallMessageFromServer(message, position))
.setNegativeButton("取消", null)
.show();
}
private void recallMessageFromServer(ChatMessage message, int position) {
String token = AuthStore.getToken(this);
if (token == null) return;
String messageId = message.getMessageId();
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/messages/" + messageId + "/recall";
Log.d(TAG, "撤回消息: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.post(RequestBody.create("", MediaType.parse("application/json")))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "撤回消息失败", e);
runOnUiThread(() -> Snackbar.make(binding.getRoot(), "撤回失败", Snackbar.LENGTH_SHORT).show());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "撤回消息响应: " + body);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
// 更新本地消息显示为已撤回
message.setMessage("[消息已撤回]");
adapter.notifyItemChanged(position);
Snackbar.make(binding.getRoot(), "消息已撤回", Snackbar.LENGTH_SHORT).show();
} else {
String errorMsg = json.optString("message", "撤回失败");
Snackbar.make(binding.getRoot(), errorMsg, Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析撤回响应失败", e);
}
});
}
});
}
private void setupInput() {
binding.sendButton.setOnClickListener(new DebounceClickListener(300) {
@Override
public void onDebouncedClick(View 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 setupReplyInput() {
// 关闭回复预览
binding.replyInputClose.setOnClickListener(v -> cancelReply());
}
/**
* 显示回复预览
*/
private void showReplyPreview(ChatMessage message) {
replyToMessage = message;
Log.d(TAG, "showReplyPreview: 设置回复消息 messageId=" + message.getMessageId() + ", content=" + message.getMessage());
binding.replyInputContainer.setVisibility(View.VISIBLE);
// 设置回复标签
String senderName = message.getUsername();
if ("".equals(senderName)) {
senderName = "自己";
}
binding.replyInputLabel.setText("回复 " + senderName);
// 设置回复内容预览
String content = message.getMessage();
if (message.getMessageType() == ChatMessage.MessageType.IMAGE) {
content = "[图片]";
} else if (message.getMessageType() == ChatMessage.MessageType.VOICE) {
content = "[语音]";
}
binding.replyInputContent.setText(content);
// 聚焦输入框
binding.messageInput.requestFocus();
}
/**
* 取消回复
*/
private void cancelReply() {
replyToMessage = null;
binding.replyInputContainer.setVisibility(View.GONE);
}
private void sendMessage() {
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
return;
}
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
if (TextUtils.isEmpty(text)) return;
if (conversationId == null) {
Snackbar.make(binding.getRoot(), "会话ID无效", Snackbar.LENGTH_SHORT).show();
return;
}
Log.d(TAG, "sendMessage: 开始发送消息, replyToMessage=" + (replyToMessage != null ? replyToMessage.getMessageId() : "null"));
// 先在本地显示消息(发送中状态)
ChatMessage newMessage = new ChatMessage("", text);
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
// 如果是回复消息,设置引用信息
if (replyToMessage != null) {
newMessage.setReplyToMessageId(replyToMessage.getMessageId());
// 如果引用的是自己的消息,显示"自己"而不是"我"
String replyToSenderName = replyToMessage.getUsername();
if ("".equals(replyToSenderName)) {
replyToSenderName = "自己";
}
newMessage.setReplyToSenderName(replyToSenderName);
newMessage.setReplyToContent(replyToMessage.getMessage());
if (replyToMessage.getMessageType() != null) {
newMessage.setReplyToMessageType(replyToMessage.getMessageType().name().toLowerCase());
}
Log.d(TAG, "设置本地引用信息: replyToId=" + replyToMessage.getMessageId()
+ ", replyToSender=" + replyToSenderName
+ ", replyToContent=" + replyToMessage.getMessage());
} else {
Log.d(TAG, "sendMessage: 没有引用消息");
}
messages.add(newMessage);
adapter.submitList(new ArrayList<>(messages));
binding.messageInput.setText("");
scrollToBottom();
// 清除回复状态
ChatMessage replyMsg = replyToMessage;
cancelReply();
// 发送到服务器
sendMessageToServer(text, newMessage, replyMsg);
}
private void sendMessageToServer(String text, ChatMessage localMessage, ChatMessage replyMsg) {
String token = AuthStore.getToken(this);
if (token == null) return;
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages";
Log.d(TAG, "发送消息: " + url);
try {
JSONObject body = new JSONObject();
body.put("message", text);
body.put("messageType", "text");
// 如果是回复消息添加引用消息ID
if (replyMsg != null && replyMsg.getMessageId() != null) {
try {
// 尝试将messageId转换为Long
long replyToId = Long.parseLong(replyMsg.getMessageId());
body.put("replyToMessageId", replyToId);
Log.d(TAG, "发送回复消息引用ID: " + replyToId
+ ", 引用内容: " + replyMsg.getMessage()
+ ", 引用发送者: " + replyMsg.getUsername());
} catch (NumberFormatException e) {
Log.w(TAG, "无法解析回复消息ID: " + replyMsg.getMessageId());
}
}
Log.d(TAG, "发送消息请求体: " + body.toString());
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "发送消息失败", e);
runOnUiThread(() -> {
Snackbar.make(binding.getRoot(), "发送失败", Snackbar.LENGTH_SHORT).show();
// 移除发送失败的消息
messages.remove(localMessage);
adapter.submitList(new ArrayList<>(messages));
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseBody = response.body() != null ? response.body().string() : "";
Log.d(TAG, "发送消息响应: " + responseBody);
runOnUiThread(() -> handleSendResponse(responseBody, localMessage));
}
});
} catch (Exception e) {
Log.e(TAG, "构建请求失败", e);
}
}
private void handleSendResponse(String responseBody, ChatMessage localMessage) {
try {
JSONObject json = new JSONObject(responseBody);
if (json.optInt("code", -1) == 200) {
JSONObject data = json.optJSONObject("data");
if (data != null) {
// 更新本地消息的ID和状态
localMessage.setMessageId(data.optString("messageId", localMessage.getMessageId()));
localMessage.setStatus(ChatMessage.MessageStatus.SENT);
adapter.notifyItemChanged(messages.indexOf(localMessage));
}
} else {
String msg = json.optString("message", "发送失败");
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
messages.remove(localMessage);
adapter.submitList(new ArrayList<>(messages));
}
} catch (Exception e) {
Log.e(TAG, "解析发送响应失败", e);
}
}
private void scrollToBottom() {
if (messages.isEmpty()) return;
binding.messagesRecyclerView.post(() -> {
int lastPosition = messages.size() - 1;
if (lastPosition >= 0) {
binding.messagesRecyclerView.smoothScrollToPosition(lastPosition);
}
});
}
/**
* 加载更多历史消息
*/
private void loadMoreHistory() {
if (isLoadingMore || !hasMoreHistory) {
return;
}
String token = AuthStore.getToken(this);
if (token == null || conversationId == null) {
return;
}
isLoadingMore = true;
historyPage++;
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId + "/messages?page=" + historyPage + "&pageSize=" + PAGE_SIZE;
Log.d(TAG, "加载更多历史消息: " + url);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "加载历史消息失败", e);
runOnUiThread(() -> {
isLoadingMore = false;
historyPage--;
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String body = response.body() != null ? response.body().string() : "";
Log.d(TAG, "历史消息响应: " + body);
runOnUiThread(() -> parseHistoryMessages(body));
}
});
}
/**
* 解析历史消息并添加到列表头部
*/
private void parseHistoryMessages(String body) {
isLoadingMore = false;
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONArray data = json.optJSONArray("data");
if (data == null || data.length() == 0) {
hasMoreHistory = false;
Snackbar.make(binding.getRoot(), "没有更多历史消息了", Snackbar.LENGTH_SHORT).show();
return;
}
// 解析历史消息
List<ChatMessage> historyMessages = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
JSONObject item = data.getJSONObject(i);
ChatMessage msg = parseChatMessage(item);
if (msg != null) {
historyMessages.add(msg);
}
}
// 消息按时间倒序返回,需要反转为正序显示
Collections.reverse(historyMessages);
if (historyMessages.isEmpty()) {
hasMoreHistory = false;
return;
}
// 保存当前滚动位置
LinearLayoutManager layoutManager = (LinearLayoutManager) binding.messagesRecyclerView.getLayoutManager();
int firstVisiblePosition = layoutManager != null ? layoutManager.findFirstVisibleItemPosition() : 0;
// 将历史消息添加到列表头部
messages.addAll(0, historyMessages);
adapter.submitList(new ArrayList<>(messages));
// 恢复滚动位置,保持用户当前查看的消息可见
if (layoutManager != null) {
layoutManager.scrollToPosition(firstVisiblePosition + historyMessages.size());
}
Log.d(TAG, "加载了 " + historyMessages.size() + " 条历史消息");
}
} catch (Exception e) {
Log.e(TAG, "解析历史消息失败", e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopPolling();
handler = null;
}
}