zhibo/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
xiao12feng8 a619bb1750 修复:首页标签分类+我的挚友处理+历史记录
优化:缘池界面+消息搜索界面
2026-01-05 16:47:07 +08:00

1179 lines
47 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.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.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 = 20;
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 String conversationId;
private int currentPage = 1;
private int initialUnreadCount = 0;
private String currentUserId;
// 对方用户信息
private int otherUserId;
private String otherUserName;
private String otherUserAvatarUrl;
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);
}
public static void start(Context context, String conversationId, String title, int otherUserId) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(EXTRA_CONVERSATION_ID, conversationId);
intent.putExtra(EXTRA_CONVERSATION_TITLE, title);
intent.putExtra("other_user_id", otherUserId);
context.startActivity(intent);
}
public static void start(Context context, String conversationId, String title, int otherUserId, String otherUserAvatarUrl) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(EXTRA_CONVERSATION_ID, conversationId);
intent.putExtra(EXTRA_CONVERSATION_TITLE, title);
intent.putExtra("other_user_id", otherUserId);
intent.putExtra("other_user_avatar_url", otherUserAvatarUrl);
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();
}
});
// 获取对方用户信息
otherUserId = getIntent() != null ? getIntent().getIntExtra("other_user_id", 0) : 0;
otherUserName = title;
otherUserAvatarUrl = getIntent() != null ? getIntent().getStringExtra("other_user_avatar_url") : null;
// 加载对方用户头像
loadOtherUserAvatar();
// 设置头像点击事件,跳转到用户主页
binding.avatarView.setOnClickListener(new DebounceClickListener() {
@Override
public void onDebouncedClick(View v) {
openUserProfile();
}
});
setupMessages();
setupInput();
// 确保输入框显示正确的提示文本
binding.messageInput.setHint("输入消息...");
// 标记会话为已读
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);
loadMessagesFromServer();
}
/**
* 加载对方用户头像
*/
private void loadOtherUserAvatar() {
// 如果已经有头像URL直接加载
if (otherUserAvatarUrl != null && !otherUserAvatarUrl.isEmpty()) {
com.bumptech.glide.Glide.with(this)
.load(otherUserAvatarUrl)
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.circleCrop()
.into(binding.avatarView);
return;
}
// 如果没有头像URL但有用户ID从服务器获取
if (otherUserId > 0) {
fetchOtherUserInfo();
}
}
/**
* 从服务器获取对方用户信息
*/
private void fetchOtherUserInfo() {
String token = AuthStore.getToken(this);
if (token == null || otherUserId <= 0) return;
String url = ApiConfig.getBaseUrl() + "/api/front/user/" + otherUserId + "/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);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONObject data = json.optJSONObject("data");
if (data != null) {
otherUserAvatarUrl = data.optString("avatar", "");
if (otherUserAvatarUrl.isEmpty()) {
otherUserAvatarUrl = data.optString("avatarUrl", "");
}
// 加载头像
if (!otherUserAvatarUrl.isEmpty() && !isFinishing() && !isDestroyed()) {
com.bumptech.glide.Glide.with(ConversationActivity.this)
.load(otherUserAvatarUrl)
.placeholder(R.drawable.ic_account_circle_24)
.error(R.drawable.ic_account_circle_24)
.circleCrop()
.into(binding.avatarView);
}
}
}
} catch (Exception e) {
Log.e(TAG, "解析对方用户信息失败", e);
}
});
}
});
}
/**
* 打开对方用户主页
*/
private void openUserProfile() {
if (otherUserId <= 0) {
// 尝试从服务器获取用户ID
int intentUserId = getIntent().getIntExtra("other_user_id", 0);
if (intentUserId > 0) {
otherUserId = intentUserId;
} else {
Snackbar.make(binding.getRoot(), "无法获取用户信息", Snackbar.LENGTH_SHORT).show();
return;
}
}
Log.d(TAG, "打开用户主页: userId=" + otherUserId + ", name=" + otherUserName + ", avatar=" + otherUserAvatarUrl);
// 跳转到用户主页
UserProfileReadOnlyActivity.start(
this,
String.valueOf(otherUserId),
otherUserName != null ? otherUserName : "用户",
"", // location
"", // bio
otherUserAvatarUrl != null ? otherUserAvatarUrl : ""
);
}
/**
* 从服务器获取当前用户ID
*/
private void fetchCurrentUserId() {
String token = AuthStore.getToken(this);
if (token == null) {
Log.w(TAG, "未登录无法获取用户ID");
return;
}
// 使用正确的用户信息接口
String url = ApiConfig.getBaseUrl() + "/api/front/user";
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) {
// 尝试多种方式获取用户ID
int uid = data.optInt("uid", 0);
if (uid == 0) {
uid = data.optInt("id", 0);
}
if (uid == 0) {
// 尝试从字符串解析
String uidStr = data.optString("uid", "");
if (!uidStr.isEmpty()) {
try {
uid = (int) Double.parseDouble(uidStr);
} catch (NumberFormatException e) {
Log.e(TAG, "解析uid失败: " + uidStr, e);
}
}
}
if (uid > 0) {
currentUserId = String.valueOf(uid);
// 保存到 AuthStore
AuthStore.setUserInfo(ConversationActivity.this, currentUserId, data.optString("nickname", data.optString("nikeName", "")));
Log.d(TAG, "从服务器获取到用户ID: " + currentUserId);
// 重新加载消息以正确显示
runOnUiThread(() -> loadMessagesFromServer());
} else {
Log.w(TAG, "无法从响应中获取用户ID: " + body);
}
}
}
} 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");
messages.clear();
if (data != null) {
for (int i = 0; i < data.length(); i++) {
JSONObject item = data.getJSONObject(i);
ChatMessage msg = parseChatMessage(item);
if (msg != null) {
messages.add(msg);
}
}
}
// 消息按时间倒序返回,需要反转为正序显示
Collections.reverse(messages);
adapter.submitList(new ArrayList<>(messages));
scrollToBottom();
} 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);
// 判断是否是自己发送的消息
// 每次都重新从 AuthStore 获取最新的 userId确保登录后能正确获取
String myUserId = AuthStore.getUserId(this);
if (myUserId == null || myUserId.isEmpty()) {
myUserId = currentUserId;
} else {
currentUserId = myUserId; // 同步更新
}
boolean isMine = false;
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
// 处理可能的浮点数格式(如 "1.0" vs "1"
try {
int myUid = (int) Double.parseDouble(myUserId);
isMine = (myUid == senderId);
} catch (NumberFormatException e) {
isMine = myUserId.equals(String.valueOf(senderId));
}
}
// 如果 senderId 为 0尝试从 senderId 字段获取
if (senderId == 0) {
senderId = item.optInt("senderId", 0);
if (myUserId != null && !myUserId.isEmpty() && senderId > 0) {
try {
int myUid = (int) Double.parseDouble(myUserId);
isMine = (myUid == senderId);
} catch (NumberFormatException e) {
isMine = myUserId.equals(String.valueOf(senderId));
}
}
}
Log.d(TAG, "消息判断: myUserId=" + myUserId + ", senderId=" + senderId + ", isMine=" + isMine);
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.setOutgoing(isMine); // 关键:设置消息方向,确保自己发送的消息显示在右侧
chatMessage.setAvatarUrl(avatarUrl);
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, "复制");
popupMenu.getMenu().add(0, 2, 0, "表情回应");
// 只有自己发送的消息才能删除和撤回
if ("".equals(message.getUsername()) || message.isOutgoing()) {
popupMenu.getMenu().add(0, 1, 0, "删除");
// 检查是否在2分钟内可以撤回
long messageTime = message.getTimestamp();
long now = System.currentTimeMillis();
long diffMinutes = (now - messageTime) / (1000 * 60);
if (diffMinutes <= 2) {
popupMenu.getMenu().add(0, 3, 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) {
showEmojiPicker(message);
return true;
} else if (item.getItemId() == 3) {
recallMessage(message, 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(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();
});
// 设置通话按钮点击事件
setupCallButtons();
}
/**
* 设置通话按钮
*/
private void setupCallButtons() {
// 语音通话按钮
binding.voiceCallButton.setOnClickListener(new DebounceClickListener() {
@Override
public void onDebouncedClick(View v) {
initiateCall("voice");
}
});
// 视频通话按钮
binding.videoCallButton.setOnClickListener(new DebounceClickListener() {
@Override
public void onDebouncedClick(View v) {
initiateCall("video");
}
});
}
/**
* 发起通话
*/
private void initiateCall(String callType) {
if (!AuthHelper.requireLoginWithToast(this, "发起通话需要登录")) {
return;
}
// 获取对方用户ID从会话中解析
int otherUserId = getOtherUserIdFromConversation();
if (otherUserId <= 0) {
// 如果无法从Intent获取尝试从服务器获取
fetchOtherUserIdFromServer(callType);
return;
}
startCallWithUserId(otherUserId, callType);
}
/**
* 使用指定的用户ID发起通话
*/
private void startCallWithUserId(int otherUserId, String callType) {
String callTypeText = "voice".equals(callType) ? "语音" : "视频";
Log.d(TAG, "发起" + callTypeText + "通话对方用户ID: " + otherUserId);
// 使用CallManager发起通话
com.example.livestreaming.call.CallManager callManager =
com.example.livestreaming.call.CallManager.getInstance(this);
// 先连接信令服务器
if (currentUserId != null && !currentUserId.isEmpty()) {
try {
int myUserId = (int) Double.parseDouble(currentUserId);
callManager.connect(myUserId);
} catch (NumberFormatException e) {
Log.e(TAG, "解析用户ID失败", e);
}
}
// 发起通话
callManager.initiateCall(otherUserId, callType,
new com.example.livestreaming.call.CallManager.CallCallback() {
@Override
public void onSuccess(com.example.livestreaming.call.InitiateCallResponse response) {
Log.d(TAG, "通话发起成功: " + response.getCallId());
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
Snackbar.make(binding.getRoot(), "呼叫失败: " + error, Snackbar.LENGTH_SHORT).show();
});
}
});
}
/**
* 从会话信息中获取对方用户ID
*/
private int getOtherUserIdFromConversation() {
// 尝试从Intent中获取
int otherUserId = getIntent().getIntExtra("other_user_id", 0);
if (otherUserId > 0) {
Log.d(TAG, "从Intent获取到对方用户ID: " + otherUserId);
return otherUserId;
}
// 如果Intent中没有尝试从服务器获取会话详情
Log.w(TAG, "Intent中没有other_user_id需要从服务器获取");
return 0;
}
/**
* 从服务器获取会话详情以获取对方用户ID
*/
private void fetchOtherUserIdFromServer(String callType) {
String token = AuthStore.getToken(this);
if (token == null || conversationId == null) {
Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show();
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/conversations/" + conversationId;
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(() -> {
try {
JSONObject json = new JSONObject(body);
if (json.optInt("code", -1) == 200) {
JSONObject data = json.optJSONObject("data");
if (data != null) {
int otherUserId = data.optInt("otherUserId", 0);
if (otherUserId > 0) {
Log.d(TAG, "从服务器获取到对方用户ID: " + otherUserId);
startCallWithUserId(otherUserId, callType);
} else {
Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show();
}
}
} else {
Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析会话详情失败", e);
Snackbar.make(binding.getRoot(), "无法获取对方用户信息", Snackbar.LENGTH_SHORT).show();
}
});
}
});
}
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;
}
// 先在本地显示消息(发送中状态)
ChatMessage newMessage = new ChatMessage("", text);
newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
messages.add(newMessage);
adapter.submitList(new ArrayList<>(messages));
binding.messageInput.setText("");
scrollToBottom();
// 发送到服务器
sendMessageToServer(text, newMessage);
}
private void sendMessageToServer(String text, ChatMessage localMessage) {
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");
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);
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
stopPolling();
handler = null;
}
/**
* 显示表情选择器
*/
private void showEmojiPicker(ChatMessage message) {
EmojiPickerBottomSheet emojiPicker = EmojiPickerBottomSheet.newInstance();
emojiPicker.setOnEmojiSelectedListener(emoji -> {
addMessageReaction(message, emoji);
});
emojiPicker.show(getSupportFragmentManager(), "emoji_picker");
}
/**
* 添加表情回应
*/
private void addMessageReaction(ChatMessage message, String emoji) {
if (!AuthHelper.requireLoginWithToast(this, "添加表情回应需要登录")) {
return;
}
String token = AuthStore.getToken(this);
if (token == null || message.getMessageId() == null) {
Snackbar.make(binding.getRoot(), "无法添加表情回应", Snackbar.LENGTH_SHORT).show();
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/messages/reactions/add";
Log.d(TAG, "添加表情回应: " + url);
try {
JSONObject body = new JSONObject();
body.put("messageId", message.getMessageId());
body.put("emoji", emoji);
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());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseBody = response.body() != null ? response.body().string() : "";
Log.d(TAG, "添加表情回应响应: " + responseBody);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(responseBody);
if (json.optInt("code", -1) == 200) {
Snackbar.make(binding.getRoot(), "已添加表情回应", Snackbar.LENGTH_SHORT).show();
// 重新加载消息以更新表情回应
loadMessageReactions(message);
} else {
String msg = json.optString("message", "添加失败");
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析响应失败", e);
}
});
}
});
} catch (Exception e) {
Log.e(TAG, "构建请求失败", e);
}
}
/**
* 移除表情回应
*/
private void removeMessageReaction(ChatMessage message, String emoji) {
if (!AuthHelper.requireLoginWithToast(this, "移除表情回应需要登录")) {
return;
}
String token = AuthStore.getToken(this);
if (token == null || message.getMessageId() == null) {
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/messages/reactions/remove";
Log.d(TAG, "移除表情回应: " + url);
try {
JSONObject body = new JSONObject();
body.put("messageId", message.getMessageId());
body.put("emoji", emoji);
Request request = new Request.Builder()
.url(url)
.addHeader("Authori-zation", token)
.method("DELETE", 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());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseBody = response.body() != null ? response.body().string() : "";
Log.d(TAG, "移除表情回应响应: " + responseBody);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(responseBody);
if (json.optInt("code", -1) == 200) {
Snackbar.make(binding.getRoot(), "已移除表情回应", Snackbar.LENGTH_SHORT).show();
// 重新加载消息以更新表情回应
loadMessageReactions(message);
} else {
String msg = json.optString("message", "移除失败");
Snackbar.make(binding.getRoot(), msg, Snackbar.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.e(TAG, "解析响应失败", e);
}
});
}
});
} catch (Exception e) {
Log.e(TAG, "构建请求失败", e);
}
}
/**
* 加载消息的表情回应列表
*/
private void loadMessageReactions(ChatMessage message) {
String token = AuthStore.getToken(this);
if (token == null || message.getMessageId() == null) {
return;
}
String url = ApiConfig.getBaseUrl() + "/api/front/messages/" + message.getMessageId() + "/reactions";
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 responseBody = response.body() != null ? response.body().string() : "";
Log.d(TAG, "表情回应响应: " + responseBody);
runOnUiThread(() -> {
try {
JSONObject json = new JSONObject(responseBody);
if (json.optInt("code", -1) == 200) {
JSONArray data = json.optJSONArray("data");
if (data != null) {
List<com.example.livestreaming.net.MessageReaction> reactions = new ArrayList<>();
for (int i = 0; i < data.length(); i++) {
JSONObject item = data.getJSONObject(i);
String emoji = item.optString("emoji", "");
int count = item.optInt("count", 0);
boolean reactedByMe = item.optBoolean("reactedByMe", false);
reactions.add(new com.example.livestreaming.net.MessageReaction(emoji, count, reactedByMe));
}
message.setReactions(reactions);
// 更新适配器
int position = messages.indexOf(message);
if (position >= 0) {
adapter.notifyItemChanged(position);
}
}
}
} catch (Exception e) {
Log.e(TAG, "解析表情回应失败", e);
}
});
}
});
}
}