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;
|
2025-12-25 17:20:34 +08:00
|
|
|
|
import android.util.Log;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import android.view.KeyEvent;
|
|
|
|
|
|
import android.view.View;
|
|
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
import androidx.annotation.NonNull;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
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;
|
2025-12-26 15:16:40 +08:00
|
|
|
|
import androidx.recyclerview.widget.RecyclerView;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
import com.example.livestreaming.databinding.ActivityConversationBinding;
|
2025-12-25 17:20:34 +08:00
|
|
|
|
import com.example.livestreaming.net.AuthStore;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
import com.google.android.material.snackbar.Snackbar;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
import org.json.JSONArray;
|
|
|
|
|
|
import org.json.JSONObject;
|
|
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import java.util.ArrayList;
|
2025-12-25 17:20:34 +08:00
|
|
|
|
import java.util.Collections;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
import okhttp3.Call;
|
|
|
|
|
|
import okhttp3.Callback;
|
|
|
|
|
|
import okhttp3.MediaType;
|
|
|
|
|
|
import okhttp3.OkHttpClient;
|
|
|
|
|
|
import okhttp3.Request;
|
|
|
|
|
|
import okhttp3.RequestBody;
|
|
|
|
|
|
import okhttp3.Response;
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
public class ConversationActivity extends AppCompatActivity {
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
private static final String TAG = "ConversationActivity";
|
2025-12-19 15:11:49 +08:00
|
|
|
|
private static final String EXTRA_CONVERSATION_ID = "extra_conversation_id";
|
|
|
|
|
|
private static final String EXTRA_CONVERSATION_TITLE = "extra_conversation_title";
|
2025-12-25 17:20:34 +08:00
|
|
|
|
private static final String EXTRA_UNREAD_COUNT = "extra_unread_count";
|
2025-12-26 15:16:40 +08:00
|
|
|
|
private static final int PAGE_SIZE = 50; // 增加每页消息数量
|
2025-12-25 19:02:07 +08:00
|
|
|
|
private static final long POLL_INTERVAL = 3000; // 3秒轮询一次
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
private ActivityConversationBinding binding;
|
2025-12-25 17:20:34 +08:00
|
|
|
|
private final OkHttpClient httpClient = new OkHttpClient();
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
private ConversationMessagesAdapter adapter;
|
|
|
|
|
|
private final List<ChatMessage> messages = new ArrayList<>();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
private Handler handler;
|
2025-12-25 19:02:07 +08:00
|
|
|
|
private Runnable pollRunnable;
|
|
|
|
|
|
private boolean isPolling = false;
|
2025-12-26 15:16:40 +08:00
|
|
|
|
private boolean isUserScrolling = false; // 用户是否正在滚动查看历史记录
|
|
|
|
|
|
private String lastMessageId = null; // 记录最后一条消息ID,用于判断是否有新消息
|
|
|
|
|
|
private boolean isLoadingMore = false; // 是否正在加载更多
|
|
|
|
|
|
private boolean hasMoreHistory = true; // 是否还有更多历史消息
|
|
|
|
|
|
private int historyPage = 1; // 历史消息页码
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
|
|
|
|
|
private String conversationId;
|
|
|
|
|
|
private int currentPage = 1;
|
|
|
|
|
|
private int initialUnreadCount = 0;
|
|
|
|
|
|
private String currentUserId;
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 回复消息相关
|
|
|
|
|
|
private ChatMessage replyToMessage = null;
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
conversationId = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_ID) : null;
|
2025-12-22 16:31:46 +08:00
|
|
|
|
initialUnreadCount = getIntent() != null ? getIntent().getIntExtra(EXTRA_UNREAD_COUNT, 0) : 0;
|
2025-12-25 19:02:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 先尝试从 AuthStore 获取 userId
|
2025-12-25 17:20:34 +08:00
|
|
|
|
currentUserId = AuthStore.getUserId(this);
|
2025-12-25 19:02:07 +08:00
|
|
|
|
Log.d(TAG, "从AuthStore获取的用户ID: " + currentUserId);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果 userId 为空,尝试从用户信息接口获取
|
|
|
|
|
|
if (currentUserId == null || currentUserId.isEmpty()) {
|
|
|
|
|
|
fetchCurrentUserId();
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-24 17:43:14 +08:00
|
|
|
|
binding.backButton.setOnClickListener(new DebounceClickListener() {
|
|
|
|
|
|
@Override
|
|
|
|
|
|
public void onDebouncedClick(View v) {
|
|
|
|
|
|
finish();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
// 确保在加载消息前获取到正确的userId
|
|
|
|
|
|
ensureUserIdLoaded();
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
setupMessages();
|
|
|
|
|
|
setupInput();
|
2025-12-26 15:16:40 +08:00
|
|
|
|
setupReplyInput();
|
2025-12-22 16:31:46 +08:00
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
// 标记会话为已读
|
|
|
|
|
|
if (initialUnreadCount > 0 && conversationId != null) {
|
|
|
|
|
|
markConversationAsRead();
|
2025-12-22 16:31:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected void onPause() {
|
|
|
|
|
|
super.onPause();
|
2025-12-25 19:02:07 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
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-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听滚动状态,判断用户是否在查看历史记录,以及加载更多历史消息
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
loadMessagesFromServer();
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 确保userId已加载,如果没有则从服务器获取
|
|
|
|
|
|
*/
|
|
|
|
|
|
private void ensureUserIdLoaded() {
|
|
|
|
|
|
// 先尝试从 AuthStore 获取
|
|
|
|
|
|
currentUserId = AuthStore.getUserId(this);
|
|
|
|
|
|
Log.d(TAG, "ensureUserIdLoaded: 从AuthStore获取的userId = " + currentUserId);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果为空,尝试从服务器获取
|
|
|
|
|
|
if (currentUserId == null || currentUserId.isEmpty()) {
|
|
|
|
|
|
fetchCurrentUserId();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 19:02:07 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从服务器获取当前用户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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从服务器加载消息列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
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");
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建新消息列表
|
|
|
|
|
|
List<ChatMessage> newMessages = new ArrayList<>();
|
2025-12-25 17:20:34 +08:00
|
|
|
|
if (data != null) {
|
|
|
|
|
|
for (int i = 0; i < data.length(); i++) {
|
|
|
|
|
|
JSONObject item = data.getJSONObject(i);
|
|
|
|
|
|
ChatMessage msg = parseChatMessage(item);
|
|
|
|
|
|
if (msg != null) {
|
2025-12-26 15:16:40 +08:00
|
|
|
|
newMessages.add(msg);
|
2025-12-25 17:20:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 消息按时间倒序返回,需要反转为正序显示
|
2025-12-26 15:16:40 +08:00
|
|
|
|
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);
|
2025-12-25 17:20:34 +08:00
|
|
|
|
adapter.submitList(new ArrayList<>(messages));
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 只有在用户不滚动查看历史记录时,且有新消息时才滚动到底部
|
|
|
|
|
|
if (!isUserScrolling && hasNewMessages) {
|
|
|
|
|
|
Log.d(TAG, "有新消息,自动滚动到底部");
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
} else if (isUserScrolling) {
|
|
|
|
|
|
Log.d(TAG, "用户正在查看历史记录,不自动滚动");
|
|
|
|
|
|
}
|
2025-12-25 17:20:34 +08:00
|
|
|
|
} 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", "");
|
2025-12-25 19:02:07 +08:00
|
|
|
|
int senderId = item.optInt("userId", 0); // 后端返回的 userId 实际是 senderId
|
2025-12-25 17:20:34 +08:00
|
|
|
|
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);
|
2025-12-26 15:16:40 +08:00
|
|
|
|
String messageType = item.optString("messageType", "text");
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 判断是否是自己发送的消息
|
2025-12-26 15:16:40 +08:00
|
|
|
|
// 使用成员变量 currentUserId,确保整个会话期间使用同一个值
|
|
|
|
|
|
String myUserId = currentUserId;
|
2025-12-25 19:02:07 +08:00
|
|
|
|
if (myUserId == null || myUserId.isEmpty()) {
|
2025-12-26 15:16:40 +08:00
|
|
|
|
// 如果成员变量为空,尝试从 AuthStore 获取
|
|
|
|
|
|
myUserId = AuthStore.getUserId(this);
|
|
|
|
|
|
if (myUserId != null && !myUserId.isEmpty()) {
|
|
|
|
|
|
currentUserId = myUserId; // 同步更新成员变量
|
|
|
|
|
|
}
|
2025-12-25 19:02:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
boolean isMine = false;
|
|
|
|
|
|
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
|
2025-12-26 15:16:40 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
2025-12-25 19:02:07 +08:00
|
|
|
|
}
|
2025-12-26 15:16:40 +08:00
|
|
|
|
Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine + ", messageId=" + messageId);
|
2025-12-25 19:02:07 +08:00
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
// 关键修复:使用 "我" 作为自己发送消息的用户名,确保显示在右侧
|
2025-12-25 17:20:34 +08:00
|
|
|
|
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);
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置消息类型
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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, "复制");
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 已撤回的消息不能被回复
|
|
|
|
|
|
if (!"[消息已撤回]".equals(message.getMessage())) {
|
|
|
|
|
|
popupMenu.getMenu().add(0, 3, 0, "回复");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 只有自己发送的消息才能删除和撤回
|
2025-12-25 17:20:34 +08:00
|
|
|
|
if ("我".equals(message.getUsername())) {
|
|
|
|
|
|
popupMenu.getMenu().add(0, 1, 0, "删除");
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否在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, "撤回");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-25 17:20:34 +08:00
|
|
|
|
}
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
popupMenu.setOnMenuItemClickListener(item -> {
|
|
|
|
|
|
if (item.getItemId() == 0) {
|
|
|
|
|
|
copyMessage(message);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} else if (item.getItemId() == 1) {
|
2025-12-25 17:20:34 +08:00
|
|
|
|
deleteMessage(message, position);
|
2025-12-23 15:37:37 +08:00
|
|
|
|
return true;
|
2025-12-26 15:16:40 +08:00
|
|
|
|
} else if (item.getItemId() == 2) {
|
|
|
|
|
|
recallMessage(message, position);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} else if (item.getItemId() == 3) {
|
|
|
|
|
|
showReplyPreview(message);
|
|
|
|
|
|
return true;
|
2025-12-23 15:37:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
private void deleteMessage(ChatMessage message, int position) {
|
2025-12-23 15:37:37 +08:00
|
|
|
|
if (position < 0 || position >= messages.size()) return;
|
|
|
|
|
|
|
|
|
|
|
|
new AlertDialog.Builder(this)
|
|
|
|
|
|
.setTitle("删除消息")
|
|
|
|
|
|
.setMessage("确定要删除这条消息吗?")
|
2025-12-25 17:20:34 +08:00
|
|
|
|
.setPositiveButton("删除", (dialog, which) -> deleteMessageFromServer(message, position))
|
2025-12-23 15:37:37 +08:00
|
|
|
|
.setNegativeButton("取消", null)
|
|
|
|
|
|
.show();
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 撤回消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
private void setupInput() {
|
2025-12-25 17:20:34 +08:00
|
|
|
|
binding.sendButton.setOnClickListener(new DebounceClickListener(300) {
|
2025-12-24 17:43:14 +08:00
|
|
|
|
@Override
|
|
|
|
|
|
public void onDebouncedClick(View v) {
|
|
|
|
|
|
sendMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置回复预览区域
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 15:11:49 +08:00
|
|
|
|
private void sendMessage() {
|
2025-12-23 18:09:56 +08:00
|
|
|
|
if (!AuthHelper.requireLoginWithToast(this, "发送私信需要登录")) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-25 17:20:34 +08:00
|
|
|
|
if (conversationId == null) {
|
|
|
|
|
|
Snackbar.make(binding.getRoot(), "会话ID无效", Snackbar.LENGTH_SHORT).show();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-19 15:11:49 +08:00
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
Log.d(TAG, "sendMessage: 开始发送消息, replyToMessage=" + (replyToMessage != null ? replyToMessage.getMessageId() : "null"));
|
|
|
|
|
|
|
2025-12-25 17:20:34 +08:00
|
|
|
|
// 先在本地显示消息(发送中状态)
|
2025-12-23 15:37:37 +08:00
|
|
|
|
ChatMessage newMessage = new ChatMessage("我", text);
|
|
|
|
|
|
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是回复消息,设置引用信息
|
|
|
|
|
|
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: 没有引用消息");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 15:37:37 +08:00
|
|
|
|
messages.add(newMessage);
|
2025-12-19 15:11:49 +08:00
|
|
|
|
adapter.submitList(new ArrayList<>(messages));
|
|
|
|
|
|
binding.messageInput.setText("");
|
|
|
|
|
|
scrollToBottom();
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 清除回复状态
|
|
|
|
|
|
ChatMessage replyMsg = replyToMessage;
|
|
|
|
|
|
cancelReply();
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 发送到服务器
|
2025-12-26 15:16:40 +08:00
|
|
|
|
sendMessageToServer(text, newMessage, replyMsg);
|
2025-12-25 17:20:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-26 15:16:40 +08:00
|
|
|
|
private void sendMessageToServer(String text, ChatMessage localMessage, ChatMessage replyMsg) {
|
2025-12-25 17:20:34 +08:00
|
|
|
|
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");
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果是回复消息,添加引用消息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());
|
2025-12-25 17:20:34 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2025-12-23 15:37:37 +08:00
|
|
|
|
}
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-26 15:16:40 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载更多历史消息
|
|
|
|
|
|
*/
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-23 15:37:37 +08:00
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
|
protected void onDestroy() {
|
|
|
|
|
|
super.onDestroy();
|
2025-12-25 19:02:07 +08:00
|
|
|
|
stopPolling();
|
2025-12-23 15:37:37 +08:00
|
|
|
|
handler = null;
|
2025-12-19 15:11:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|