Azure deployment setup #297
@@ -10,11 +10,18 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
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.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -22,6 +29,7 @@ import androidx.core.view.GravityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ChatAdapter;
|
||||
@@ -45,7 +53,8 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
@@ -65,10 +74,12 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
private FragmentChatBinding binding;
|
||||
private ChatListViewModel viewModel;
|
||||
|
||||
private ChatAdapter chatAdapter;
|
||||
private ChatAdapter activeChatAdapter;
|
||||
private ChatAdapter closedChatAdapter;
|
||||
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 Uri pendingAttachmentUri;
|
||||
|
||||
@@ -82,7 +93,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
viewModel = new ViewModelProvider(this).get(ChatListViewModel.class);
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(ChatListViewModel.class);
|
||||
attachmentLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
@@ -117,6 +128,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
binding.btnAttach.setOnClickListener(v -> selectAttachment());
|
||||
binding.btnRemoveAttachment.setOnClickListener(v -> removeAttachment());
|
||||
|
||||
setupDrawerToggles();
|
||||
setupRecyclerViews();
|
||||
observeViewModel();
|
||||
loadInitialData();
|
||||
@@ -124,10 +136,36 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
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() {
|
||||
chatAdapter = new ChatAdapter(chatList, this);
|
||||
binding.rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.rvChatList.setAdapter(chatAdapter);
|
||||
activeChatAdapter = new ChatAdapter(activeChatList, this);
|
||||
binding.rvActiveChats.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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.setBaseUrl(baseUrl);
|
||||
@@ -144,7 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
lm.setStackFromEnd(true);
|
||||
binding.rvMessages.setLayoutManager(lm);
|
||||
binding.rvMessages.setAdapter(messageAdapter);
|
||||
setConversationActive(false);
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
|
||||
private void showFullScreenImage(Message message) {
|
||||
@@ -226,19 +264,18 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
|
||||
private void observeViewModel() {
|
||||
viewModel.getChatList().observe(getViewLifecycleOwner(), list -> {
|
||||
chatList.clear();
|
||||
chatList.addAll(list);
|
||||
chatAdapter.notifyDataSetChanged();
|
||||
viewModel.getActiveChats().observe(getViewLifecycleOwner(), list -> {
|
||||
activeChatList.clear();
|
||||
activeChatList.addAll(list);
|
||||
activeChatAdapter.notifyDataSetChanged();
|
||||
updateTitleAndStateIfActive(list);
|
||||
});
|
||||
|
||||
if (activeConversationId != null) {
|
||||
for (Chat chat : list) {
|
||||
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.getClosedChats().observe(getViewLifecycleOwner(), list -> {
|
||||
closedChatList.clear();
|
||||
closedChatList.addAll(list);
|
||||
closedChatAdapter.notifyDataSetChanged();
|
||||
updateTitleAndStateIfActive(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() {
|
||||
String token = tokenManager.getToken();
|
||||
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")) {
|
||||
activeConversationId = getActivity().getIntent().getLongExtra("conversation_id", -1);
|
||||
getActivity().getIntent().removeExtra("conversation_id");
|
||||
} else {
|
||||
// Restore last active conversation if any
|
||||
activeConversationId = viewModel.getLastActiveConversationId();
|
||||
}
|
||||
|
||||
viewModel.loadCustomers();
|
||||
|
||||
//
|
||||
if (activeConversationId != null) {
|
||||
setConversationActive(true);
|
||||
// Re-subscribe and load history if there is an active conversation ID
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
} else {
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
setConversationActive(true);
|
||||
viewModel.setLastActiveConversationId(activeConversationId);
|
||||
|
||||
setConversationActive(true, chat.getStatus());
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
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(), ""));
|
||||
// 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(() -> {
|
||||
viewModel.updateConversationLocally(dto);
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
setConversationActive(true, dto.getStatus());
|
||||
binding.tvChatTitle.setText(viewModel.getCustomerName(dto.getCustomerId()));
|
||||
}
|
||||
});
|
||||
@@ -424,8 +479,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
}
|
||||
}
|
||||
|
||||
private void setConversationActive(boolean active) {
|
||||
UIUtils.setViewsEnabled(active, binding.btnSend, binding.etMessage, binding.btnAttach);
|
||||
private void setConversationActive(boolean active, String status) {
|
||||
boolean isClosed = "CLOSED".equalsIgnoreCase(status);
|
||||
UIUtils.setViewsEnabled(active && !isClosed, binding.btnSend, binding.etMessage, binding.btnAttach);
|
||||
|
||||
if (!active) {
|
||||
activeConversationId = null;
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
@@ -437,7 +494,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
binding.etMessage.setText("");
|
||||
binding.etMessage.setHint("Select a chat to start messaging");
|
||||
} else {
|
||||
binding.etMessage.setHint("Type a message...");
|
||||
binding.etMessage.setHint(isClosed ? "This chat is closed" : "Type a message...");
|
||||
ChatNotificationService.activeConversationIdInUi = activeConversationId;
|
||||
}
|
||||
}
|
||||
@@ -449,4 +506,4 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (stompChatManager != null) stompChatManager.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ public class Chat {
|
||||
private String lastMessage;
|
||||
private Long customerId;
|
||||
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.customerName = customerName;
|
||||
this.lastMessage = lastMessage;
|
||||
this.customerId = customerId;
|
||||
this.staffId = staffId;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getChatId() {
|
||||
@@ -34,4 +36,8 @@ public class Chat {
|
||||
public Long getStaffId() {
|
||||
return staffId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -32,10 +33,13 @@ public class ChatListViewModel extends ViewModel {
|
||||
private final ChatRepository chatRepository;
|
||||
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 Map<Long, String> customerNames = new HashMap<>();
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private Long lastActiveConversationId = null;
|
||||
|
||||
@Inject
|
||||
public ChatListViewModel(ChatRepository chatRepository, CustomerRepository customerRepository) {
|
||||
@@ -43,10 +47,19 @@ public class ChatListViewModel extends ViewModel {
|
||||
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<Boolean> getIsLoading() { return isLoading; }
|
||||
|
||||
public Long getLastActiveConversationId() {
|
||||
return lastActiveConversationId;
|
||||
}
|
||||
|
||||
public void setLastActiveConversationId(Long conversationId) {
|
||||
this.lastActiveConversationId = conversationId;
|
||||
}
|
||||
|
||||
public void loadCustomers() {
|
||||
customerRepository.getAllCustomers(0, 100).observeForever(resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
@@ -62,12 +75,19 @@ public class ChatListViewModel extends ViewModel {
|
||||
isLoading.setValue(true);
|
||||
chatRepository.getAllConversations().observeForever(resource -> {
|
||||
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) {
|
||||
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);
|
||||
} else if (resource != null && resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
@@ -106,21 +126,24 @@ public class ChatListViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public void updateConversationLocally(ConversationDTO dto) {
|
||||
List<Chat> current = new ArrayList<>(chatList.getValue());
|
||||
boolean updated = false;
|
||||
String name = customerNames.getOrDefault(dto.getCustomerId(), "Customer #" + dto.getCustomerId());
|
||||
updateList(activeChats, dto);
|
||||
updateList(closedChats, dto);
|
||||
|
||||
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++) {
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updated) {
|
||||
current.add(0, new Chat(String.valueOf(dto.getId()), name, dto.getLastMessage(), dto.getCustomerId(), dto.getStaffId()));
|
||||
}
|
||||
chatList.setValue(current);
|
||||
if (updated) liveData.setValue(current);
|
||||
}
|
||||
|
||||
private Message dtoToModel(MessageDTO dto) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/chatDrawerLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -125,29 +126,101 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/chatListDrawer"
|
||||
android:layout_width="260dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/primary_dark"
|
||||
android:padding="16dp">
|
||||
android:background="@color/primary_dark">
|
||||
|
||||
<TextView
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Active Chats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="24dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvChatList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/headerActiveChats"
|
||||
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: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>
|
||||
Reference in New Issue
Block a user