1179 lines
47 KiB
Java
1179 lines
47 KiB
Java
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);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|