made chat more user frendly

This commit is contained in:
Alex
2026-04-09 19:47:43 -06:00
parent 9b4aad0c36
commit c2faeb06ce
4 changed files with 218 additions and 59 deletions

View File

@@ -10,11 +10,18 @@ import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Log; import android.util.Log;
import android.view.*; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -22,6 +29,7 @@ import androidx.core.view.GravityCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.example.petstoremobile.R; import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ChatAdapter; import com.example.petstoremobile.adapters.ChatAdapter;
@@ -45,7 +53,8 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.*; import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@@ -65,10 +74,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private FragmentChatBinding binding; private FragmentChatBinding binding;
private ChatListViewModel viewModel; private ChatListViewModel viewModel;
private ChatAdapter chatAdapter; private ChatAdapter activeChatAdapter;
private ChatAdapter closedChatAdapter;
private MessageAdapter messageAdapter; private MessageAdapter messageAdapter;
private final List<Chat> chatList = new ArrayList<>(); private final List<Chat> activeChatList = new ArrayList<>();
private final List<Chat> closedChatList = new ArrayList<>();
private final List<Message> messageList = new ArrayList<>(); private final List<Message> messageList = new ArrayList<>();
private Uri pendingAttachmentUri; private Uri pendingAttachmentUri;
@@ -82,7 +93,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ChatListViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.class);
attachmentLauncher = registerForActivityResult( attachmentLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
@@ -117,6 +128,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.btnAttach.setOnClickListener(v -> selectAttachment()); binding.btnAttach.setOnClickListener(v -> selectAttachment());
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment()); binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
setupDrawerToggles();
setupRecyclerViews(); setupRecyclerViews();
observeViewModel(); observeViewModel();
loadInitialData(); loadInitialData();
@@ -124,10 +136,36 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
return binding.getRoot(); return binding.getRoot();
} }
private void setupDrawerToggles() {
binding.headerActiveChats.setOnClickListener(v -> {
if (binding.rvActiveChats.getVisibility() == View.VISIBLE) {
binding.rvActiveChats.setVisibility(View.GONE);
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_down_float);
} else {
binding.rvActiveChats.setVisibility(View.VISIBLE);
binding.ivActiveChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
binding.headerClosedChats.setOnClickListener(v -> {
if (binding.rvClosedChats.getVisibility() == View.VISIBLE) {
binding.rvClosedChats.setVisibility(View.GONE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_down_float);
} else {
binding.rvClosedChats.setVisibility(View.VISIBLE);
binding.ivClosedChevron.setImageResource(android.R.drawable.arrow_up_float);
}
});
}
private void setupRecyclerViews() { private void setupRecyclerViews() {
chatAdapter = new ChatAdapter(chatList, this); activeChatAdapter = new ChatAdapter(activeChatList, this);
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvChatList.setAdapter(chatAdapter); binding.rvActiveChats.setAdapter(activeChatAdapter);
closedChatAdapter = new ChatAdapter(closedChatList, this);
binding.rvClosedChats.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvClosedChats.setAdapter(closedChatAdapter);
messageAdapter = new MessageAdapter(messageList, null); messageAdapter = new MessageAdapter(messageList, null);
messageAdapter.setBaseUrl(baseUrl); messageAdapter.setBaseUrl(baseUrl);
@@ -144,7 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
lm.setStackFromEnd(true); lm.setStackFromEnd(true);
binding.rvMessages.setLayoutManager(lm); binding.rvMessages.setLayoutManager(lm);
binding.rvMessages.setAdapter(messageAdapter); binding.rvMessages.setAdapter(messageAdapter);
setConversationActive(false); setConversationActive(false, null);
} }
private void showFullScreenImage(Message message) { private void showFullScreenImage(Message message) {
@@ -226,19 +264,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} }
private void observeViewModel() { private void observeViewModel() {
viewModel.getChatList().observe(getViewLifecycleOwner(), list -> { viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> {
chatList.clear(); activeChatList.clear();
chatList.addAll(list); activeChatList.addAll(list);
chatAdapter.notifyDataSetChanged(); activeChatAdapter.notifyDataSetChanged();
updateTitleAndStateIfActive(list);
});
if (activeConversationId != null) { viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> {
for (Chat chat : list) { closedChatList.clear();
if (chat.getChatId().equals(String.valueOf(activeConversationId))) { closedChatList.addAll(list);
binding.tvChatTitle.setText(chat.getCustomerName()); closedChatAdapter.notifyDataSetChanged();
break; updateTitleAndStateIfActive(list);
}
}
}
}); });
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> { viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
@@ -253,6 +290,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
}); });
} }
private void updateTitleAndStateIfActive(List<Chat> list) {
if (activeConversationId != null) {
for (Chat chat : list) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
binding.tvChatTitle.setText(chat.getCustomerName());
setConversationActive(true, chat.getStatus());
break;
}
}
}
}
private void loadInitialData() { private void loadInitialData() {
String token = tokenManager.getToken(); String token = tokenManager.getToken();
Long currentUserId = tokenManager.getUserId(); Long currentUserId = tokenManager.getUserId();
@@ -274,21 +323,29 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) { } else if (getActivity() != null && getActivity().getIntent().hasExtra("conversation_id")) {
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1); activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
getActivity().getIntent().removeExtra("conversation_id"); getActivity().getIntent().removeExtra("conversation_id");
} else {
// Restore last active conversation if any
activeConversationId = viewModel.getLastActiveConversationId();
} }
viewModel.loadCustomers(); viewModel.loadCustomers();
//
if (activeConversationId != null) { if (activeConversationId != null) {
setConversationActive(true); // Re-subscribe and load history if there is an active conversation ID
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId); if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
viewModel.loadMessageHistory(activeConversationId); viewModel.loadMessageHistory(activeConversationId);
} else {
setConversationActive(false, null);
} }
} }
@Override @Override
public void onChatClick(Chat chat) { public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId()); activeConversationId = Long.parseLong(chat.getChatId());
setConversationActive(true); viewModel.setLastActiveConversationId(activeConversationId);
setConversationActive(true, chat.getStatus());
binding.tvChatTitle.setText(chat.getCustomerName()); binding.tvChatTitle.setText(chat.getCustomerName());
binding.chatDrawerLayout.closeDrawer(GravityCompat.START); binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
@@ -377,8 +434,6 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} }
} }
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), "")); viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
// Re-load coversations to get correct names if needed or just let local update handle it
viewModel.loadConversations();
}); });
} }
@@ -387,7 +442,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
viewModel.updateConversationLocally(dto); viewModel.updateConversationLocally(dto);
if (activeConversationId != null && activeConversationId.equals(dto.getId())) { if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true); setConversationActive(true, dto.getStatus());
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId())); binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
} }
}); });
@@ -424,8 +479,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
} }
} }
private void setConversationActive(boolean active) { private void setConversationActive(boolean active, String status) {
UIUtils.setViewsEnabled(active, binding.btnSend, binding.etMessage, binding.btnAttach); boolean isClosed = "CLOSED".equalsIgnoreCase(status);
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
if (!active) { if (!active) {
activeConversationId = null; activeConversationId = null;
ChatNotificationService.activeConversationIdInUi = null; ChatNotificationService.activeConversationIdInUi = null;
@@ -437,7 +494,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
binding.etMessage.setText(""); binding.etMessage.setText("");
binding.etMessage.setHint("Select a chat to start messaging"); binding.etMessage.setHint("Select a chat to start messaging");
} else { } else {
binding.etMessage.setHint("Type a message..."); binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message...");
ChatNotificationService.activeConversationIdInUi = activeConversationId; ChatNotificationService.activeConversationIdInUi = activeConversationId;
} }
} }
@@ -449,4 +506,4 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
ChatNotificationService.activeConversationIdInUi = null; ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect(); if (stompChatManager != null) stompChatManager.disconnect();
} }
} }

View File

@@ -6,13 +6,15 @@ public class Chat {
private String lastMessage; private String lastMessage;
private Long customerId; private Long customerId;
private Long staffId; private Long staffId;
private String status;
public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId, String status) {
this.chatId = chatId; this.chatId = chatId;
this.customerName = customerName; this.customerName = customerName;
this.lastMessage = lastMessage; this.lastMessage = lastMessage;
this.customerId = customerId; this.customerId = customerId;
this.staffId = staffId; this.staffId = staffId;
this.status = status;
} }
public String getChatId() { public String getChatId() {
@@ -34,4 +36,8 @@ public class Chat {
public Long getStaffId() { public Long getStaffId() {
return staffId; return staffId;
} }
public String getStatus() {
return status;
}
} }

View File

@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
@@ -32,10 +33,13 @@ public class ChatListViewModel extends ViewModel {
private final ChatRepository chatRepository; private final ChatRepository chatRepository;
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
private final MutableLiveData<List<Chat>> chatList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<Chat>> activeChats = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Chat>> closedChats = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<List<Message>> messageList = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData<List<Message>> messageList = new MutableLiveData<>(new ArrayList<>());
private final Map<Long, String> customerNames = new HashMap<>(); private final Map<Long, String> customerNames = new HashMap<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false); private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private Long lastActiveConversationId = null;
@Inject @Inject
public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) { public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) {
@@ -43,10 +47,19 @@ public class ChatListViewModel extends ViewModel {
this.customerRepository = customerRepository; this.customerRepository = customerRepository;
} }
public LiveData<List<Chat>> getChatList() { return chatList; } public LiveData<List<Chat>> getActiveChats() { return activeChats; }
public LiveData<List<Chat>> getClosedChats() { return closedChats; }
public LiveData<List<Message>> getMessageList() { return messageList; } public LiveData<List<Message>> getMessageList() { return messageList; }
public LiveData<Boolean> getIsLoading() { return isLoading; } public LiveData<Boolean> getIsLoading() { return isLoading; }
public Long getLastActiveConversationId() {
return lastActiveConversationId;
}
public void setLastActiveConversationId(Long conversationId) {
this.lastActiveConversationId = conversationId;
}
public void loadCustomers() { public void loadCustomers() {
customerRepository.getAllCustomers(0, 100).observeForever(resource -> { customerRepository.getAllCustomers(0, 100).observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
@@ -62,12 +75,19 @@ public class ChatListViewModel extends ViewModel {
isLoading.setValue(true); isLoading.setValue(true);
chatRepository.getAllConversations().observeForever(resource -> { chatRepository.getAllConversations().observeForever(resource -> {
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
List<Chat> chats = new ArrayList<>(); List<Chat> active = new ArrayList<>();
List<Chat> closed = new ArrayList<>();
for (ConversationDTO dto : resource.data) { for (ConversationDTO dto : resource.data) {
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId()); String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
chats.add(new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); Chat chat = new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus());
if ("CLOSED".equalsIgnoreCase(dto.getStatus())) {
closed.add(chat);
} else {
active.add(chat);
}
} }
chatList.setValue(chats); activeChats.setValue(active);
closedChats.setValue(closed);
isLoading.setValue(false); isLoading.setValue(false);
} else if (resource != null && resource.status == Resource.Status.ERROR) { } else if (resource != null && resource.status == Resource.Status.ERROR) {
isLoading.setValue(false); isLoading.setValue(false);
@@ -106,21 +126,24 @@ public class ChatListViewModel extends ViewModel {
} }
public void updateConversationLocally(ConversationDTO dto) { public void updateConversationLocally(ConversationDTO dto) {
List<Chat> current = new ArrayList<>(chatList.getValue()); updateList(activeChats, dto);
boolean updated = false; updateList(closedChats, dto);
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
loadConversations();
}
private void updateList(MutableLiveData<List<Chat>> liveData, ConversationDTO dto) {
List<Chat> current = new ArrayList<>(liveData.getValue());
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
boolean updated = false;
for (int i = 0; i < current.size(); i++) { for (int i = 0; i < current.size(); i++) {
if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) { if (current.get(i).getChatId().equals(String.valueOf(dto.getId()))) {
current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId())); current.set(i, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId(), dto.getStatus()));
updated = true; updated = true;
break; break;
} }
} }
if (!updated) { if (updated) liveData.setValue(current);
current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
}
chatList.setValue(current);
} }
private Message dtoToModel(MessageDTO dto) { private Message dtoToModel(MessageDTO dto) {

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/chatDrawerLayout" android:id="@+id/chatDrawerLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -125,29 +126,101 @@
</LinearLayout> </LinearLayout>
<LinearLayout <androidx.core.widget.NestedScrollView
android:id="@+id/chatListDrawer" android:id="@+id/chatListDrawer"
android:layout_width="260dp" android:layout_width="260dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:orientation="vertical" android:background="@color/primary_dark">
android:background="@color/primary_dark"
android:padding="16dp">
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Active Chats" android:orientation="vertical"
android:textColor="@color/white" android:paddingTop="24dp">
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView <LinearLayout
android:id="@+id/rvChatList" android:id="@+id/headerActiveChats"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
</LinearLayout> <TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="ACTIVE CHATS"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:textStyle="bold"/>
<ImageView
android:id="@+id/ivActiveChevron"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/arrow_up_float"
app:tint="@color/text_light"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvActiveChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:layout_marginBottom="16dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
<LinearLayout
android:id="@+id/headerClosedChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="CLOSED CHATS"
android:textColor="@color/text_light"
android:textSize="11sp"
android:letterSpacing="0.15"
android:textStyle="bold"/>
<ImageView
android:id="@+id/ivClosedChevron"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/arrow_down_float"
app:tint="@color/text_light"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvClosedChats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>