mirror of
https://github.com/gitnex-org/gitnex.git
synced 2026-05-06 08:56:02 -05:00
User mentions in issues, prs and comments (#1418)
Closes #1394 Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1418 Co-authored-by: M M Arif <mmarif@swatian.com> Co-committed-by: M M Arif <mmarif@swatian.com>
This commit is contained in:
@@ -60,6 +60,7 @@ import org.mian.gitnex.helpers.AlertDialogs;
|
||||
import org.mian.gitnex.helpers.AppDatabaseSettings;
|
||||
import org.mian.gitnex.helpers.Constants;
|
||||
import org.mian.gitnex.helpers.Markdown;
|
||||
import org.mian.gitnex.helpers.MentionHelper;
|
||||
import org.mian.gitnex.helpers.SnackBar;
|
||||
import org.mian.gitnex.helpers.Toasty;
|
||||
import org.mian.gitnex.helpers.attachments.AttachmentUtils;
|
||||
@@ -98,6 +99,7 @@ public class CreateIssueActivity extends BaseActivity
|
||||
private NotesApi notesApi;
|
||||
private List<Notes> notesList;
|
||||
public AlertDialog dialogNotes;
|
||||
private MentionHelper mentionHelper;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
@@ -152,6 +154,9 @@ public class CreateIssueActivity extends BaseActivity
|
||||
contentUri.clear();
|
||||
});
|
||||
|
||||
mentionHelper = new MentionHelper(this, viewBinding.newIssueDescription);
|
||||
mentionHelper.setup();
|
||||
|
||||
viewBinding.topAppBar.setOnMenuItemClickListener(
|
||||
menuItem -> {
|
||||
int id = menuItem.getItemId();
|
||||
@@ -280,8 +285,9 @@ public class CreateIssueActivity extends BaseActivity
|
||||
});
|
||||
|
||||
public void onDestroy() {
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
super.onDestroy();
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
mentionHelper.dismissPopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,6 +61,7 @@ import org.mian.gitnex.helpers.AlertDialogs;
|
||||
import org.mian.gitnex.helpers.AppDatabaseSettings;
|
||||
import org.mian.gitnex.helpers.Constants;
|
||||
import org.mian.gitnex.helpers.Markdown;
|
||||
import org.mian.gitnex.helpers.MentionHelper;
|
||||
import org.mian.gitnex.helpers.SnackBar;
|
||||
import org.mian.gitnex.helpers.Toasty;
|
||||
import org.mian.gitnex.helpers.attachments.AttachmentUtils;
|
||||
@@ -97,6 +98,7 @@ public class CreatePullRequestActivity extends BaseActivity
|
||||
private NotesApi notesApi;
|
||||
private List<Notes> notesList;
|
||||
public AlertDialog dialogNotes;
|
||||
private MentionHelper mentionHelper;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
@@ -123,6 +125,9 @@ public class CreatePullRequestActivity extends BaseActivity
|
||||
|
||||
int resultLimit = Constants.getCurrentResultLimit(ctx);
|
||||
|
||||
mentionHelper = new MentionHelper(this, viewBinding.prBody);
|
||||
mentionHelper.setup();
|
||||
|
||||
viewBinding.prBody.setOnTouchListener(
|
||||
(touchView, motionEvent) -> {
|
||||
touchView.getParent().requestDisallowInterceptTouchEvent(true);
|
||||
@@ -290,8 +295,9 @@ public class CreatePullRequestActivity extends BaseActivity
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
super.onDestroy();
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
mentionHelper.dismissPopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,6 +70,7 @@ import org.mian.gitnex.helpers.AppDatabaseSettings;
|
||||
import org.mian.gitnex.helpers.AppUtil;
|
||||
import org.mian.gitnex.helpers.Constants;
|
||||
import org.mian.gitnex.helpers.Markdown;
|
||||
import org.mian.gitnex.helpers.MentionHelper;
|
||||
import org.mian.gitnex.helpers.SnackBar;
|
||||
import org.mian.gitnex.helpers.attachments.AttachmentUtils;
|
||||
import org.mian.gitnex.helpers.attachments.AttachmentsModel;
|
||||
@@ -101,6 +102,7 @@ public class EditIssueActivity extends BaseActivity
|
||||
private static List<AttachmentsModel> attachmentsList;
|
||||
private static final List<Uri> contentUri = new ArrayList<>();
|
||||
private MenuItem create;
|
||||
private MentionHelper mentionHelper;
|
||||
|
||||
public ActivityResultLauncher<Intent> downloadAttachmentLauncher =
|
||||
registerForActivityResult(
|
||||
@@ -229,8 +231,9 @@ public class EditIssueActivity extends BaseActivity
|
||||
});
|
||||
|
||||
public void onDestroy() {
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
super.onDestroy();
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
mentionHelper.dismissPopup();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -264,6 +267,9 @@ public class EditIssueActivity extends BaseActivity
|
||||
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(this);
|
||||
|
||||
mentionHelper = new MentionHelper(this, binding.editIssueDescription);
|
||||
mentionHelper.setup();
|
||||
|
||||
create = binding.topAppBar.getMenu().getItem(2);
|
||||
create.setTitle(getString(R.string.saveButton));
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ import org.mian.gitnex.helpers.ColorInverter;
|
||||
import org.mian.gitnex.helpers.Constants;
|
||||
import org.mian.gitnex.helpers.LabelWidthCalculator;
|
||||
import org.mian.gitnex.helpers.Markdown;
|
||||
import org.mian.gitnex.helpers.MentionHelper;
|
||||
import org.mian.gitnex.helpers.SnackBar;
|
||||
import org.mian.gitnex.helpers.TimeHelper;
|
||||
import org.mian.gitnex.helpers.TinyDB;
|
||||
@@ -167,6 +168,7 @@ public class IssueDetailActivity extends BaseActivity
|
||||
private final float buttonAlphaStatDisabled = .5F;
|
||||
private final float buttonAlphaStatEnabled = 1F;
|
||||
private int loadingFinished = 0;
|
||||
private MentionHelper mentionHelper;
|
||||
|
||||
private enum Mode {
|
||||
EDIT,
|
||||
@@ -411,6 +413,9 @@ public class IssueDetailActivity extends BaseActivity
|
||||
startActivity(issue.getIntent(ctx, DiffActivity.class));
|
||||
}
|
||||
|
||||
mentionHelper = new MentionHelper(this, viewBinding.commentReply);
|
||||
mentionHelper.setup();
|
||||
|
||||
viewBinding.commentReply.addTextChangedListener(
|
||||
new TextWatcher() {
|
||||
@Override
|
||||
@@ -545,8 +550,9 @@ public class IssueDetailActivity extends BaseActivity
|
||||
}
|
||||
|
||||
public void onDestroy() {
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
super.onDestroy();
|
||||
AttachmentsAdapter.setAttachmentsReceiveListener(null);
|
||||
mentionHelper.dismissPopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
305
app/src/main/java/org/mian/gitnex/helpers/MentionHelper.java
Normal file
305
app/src/main/java/org/mian/gitnex/helpers/MentionHelper.java
Normal file
@@ -0,0 +1,305 @@
|
||||
package org.mian.gitnex.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.gitnex.tea4j.v2.models.InlineResponse2001;
|
||||
import org.gitnex.tea4j.v2.models.User;
|
||||
import org.mian.gitnex.R;
|
||||
import org.mian.gitnex.clients.RetrofitClient;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* @author mmarif
|
||||
*/
|
||||
public class MentionHelper {
|
||||
|
||||
private final PopupWindow mentionPopup = new PopupWindow();
|
||||
private final MentionAdapter mentionAdapter;
|
||||
private final List<User> mentionSuggestions = new ArrayList<>();
|
||||
private final Context context;
|
||||
private final EditText editText;
|
||||
|
||||
public MentionHelper(Context context, EditText editText) {
|
||||
|
||||
this.context = context;
|
||||
this.editText = editText;
|
||||
|
||||
RecyclerView mentionRecyclerView = new RecyclerView(context);
|
||||
mentionRecyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
mentionAdapter =
|
||||
new MentionAdapter(
|
||||
context,
|
||||
mentionSuggestions,
|
||||
user -> {
|
||||
String mention = "@" + user.getLogin();
|
||||
int cursorPos = editText.getSelectionStart();
|
||||
String text = editText.getText().toString();
|
||||
int mentionStart = text.lastIndexOf("@", cursorPos - 1);
|
||||
if (mentionStart != -1) {
|
||||
editText.getText().replace(mentionStart, cursorPos, mention + " ");
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
});
|
||||
mentionRecyclerView.setAdapter(mentionAdapter);
|
||||
|
||||
int paddingDp = 4;
|
||||
int sidePaddingDp = 12;
|
||||
int paddingPx = (int) (paddingDp * context.getResources().getDisplayMetrics().density);
|
||||
int sidePaddingPx =
|
||||
(int) (sidePaddingDp * context.getResources().getDisplayMetrics().density);
|
||||
mentionRecyclerView.setPadding(sidePaddingPx, paddingPx, sidePaddingPx, paddingPx);
|
||||
|
||||
mentionPopup.setContentView(mentionRecyclerView);
|
||||
mentionPopup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mentionPopup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
mentionPopup.setFocusable(false);
|
||||
mentionPopup.setBackgroundDrawable(
|
||||
ContextCompat.getDrawable(context, R.drawable.shape_round_corners));
|
||||
mentionPopup.setElevation(8f);
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
|
||||
editText.addTextChangedListener(
|
||||
new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(
|
||||
CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
handleMentionInput(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
}
|
||||
|
||||
public void dismissPopup() {
|
||||
if (mentionPopup.isShowing()) {
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMentionInput(CharSequence text) {
|
||||
|
||||
int cursorPos = editText.getSelectionStart();
|
||||
|
||||
if (cursorPos > 0) {
|
||||
|
||||
String beforeCursor = text.subSequence(0, cursorPos).toString();
|
||||
int atIndex = beforeCursor.lastIndexOf("@");
|
||||
|
||||
if (atIndex != -1
|
||||
&& (cursorPos == atIndex + 1
|
||||
|| !beforeCursor.substring(atIndex).contains(" "))) {
|
||||
|
||||
String query = beforeCursor.substring(atIndex + 1);
|
||||
if (!query.isEmpty()) {
|
||||
fetchUserSuggestions(query);
|
||||
} else {
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
} else {
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
} else {
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchUserSuggestions(String query) {
|
||||
|
||||
Call<InlineResponse2001> call =
|
||||
RetrofitClient.getApiInterface(context).userSearch(query, null, 1, 4);
|
||||
|
||||
call.enqueue(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public void onResponse(
|
||||
@NonNull Call<InlineResponse2001> call,
|
||||
@NonNull Response<InlineResponse2001> response) {
|
||||
|
||||
if (response.isSuccessful()
|
||||
&& response.body() != null
|
||||
&& response.body().isOk()) {
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
List<User> newData = response.body().getData();
|
||||
if (newData != null && !newData.isEmpty()) {
|
||||
mentionSuggestions.addAll(
|
||||
newData.subList(0, Math.min(newData.size(), 4)));
|
||||
mentionAdapter.notifyItemRangeInserted(
|
||||
0, mentionSuggestions.size());
|
||||
showMentionPopup();
|
||||
} else {
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
} else {
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(
|
||||
@NonNull Call<InlineResponse2001> call, @NonNull Throwable t) {
|
||||
|
||||
int oldSize = mentionSuggestions.size();
|
||||
mentionSuggestions.clear();
|
||||
if (oldSize > 0) {
|
||||
mentionAdapter.notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
mentionPopup.dismiss();
|
||||
Log.e("MentionHelper", "Failed to fetch users: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showMentionPopup() {
|
||||
|
||||
int[] location = new int[2];
|
||||
editText.getLocationOnScreen(location);
|
||||
int x = location[0];
|
||||
int y = location[1] - 24;
|
||||
|
||||
int popupWidth = editText.getWidth();
|
||||
int popupHeight = calculatePopupHeight();
|
||||
|
||||
if (mentionPopup.isShowing()) {
|
||||
mentionPopup.dismiss();
|
||||
}
|
||||
|
||||
mentionPopup.setWidth(popupWidth);
|
||||
mentionPopup.setHeight(popupHeight);
|
||||
mentionPopup.showAtLocation(editText, Gravity.NO_GRAVITY, x, y - popupHeight);
|
||||
}
|
||||
|
||||
private int calculatePopupHeight() {
|
||||
|
||||
int itemHeightDp = 48;
|
||||
int paddingDp = 4;
|
||||
int itemHeightPx =
|
||||
(int) (itemHeightDp * context.getResources().getDisplayMetrics().density);
|
||||
int paddingPx = (int) (paddingDp * context.getResources().getDisplayMetrics().density);
|
||||
|
||||
int contentHeightPx = mentionSuggestions.size() * itemHeightPx;
|
||||
int totalPaddingPx = paddingPx * 2;
|
||||
int popupHeight = contentHeightPx + totalPaddingPx;
|
||||
|
||||
int maxHeightPx = 4 * itemHeightPx + totalPaddingPx;
|
||||
popupHeight = Math.min(popupHeight, maxHeightPx);
|
||||
|
||||
if (mentionSuggestions.isEmpty()) {
|
||||
popupHeight = itemHeightPx + totalPaddingPx;
|
||||
}
|
||||
|
||||
return popupHeight;
|
||||
}
|
||||
|
||||
private static class MentionAdapter
|
||||
extends RecyclerView.Adapter<MentionAdapter.MentionViewHolder> {
|
||||
private final Context context;
|
||||
private final List<User> users;
|
||||
private final OnUserClickListener listener;
|
||||
|
||||
public MentionAdapter(Context context, List<User> users, OnUserClickListener listener) {
|
||||
this.context = context;
|
||||
this.users = users;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull @Override
|
||||
public MentionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view =
|
||||
LayoutInflater.from(context)
|
||||
.inflate(R.layout.list_users_mention, parent, false);
|
||||
return new MentionViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(MentionViewHolder holder, int position) {
|
||||
|
||||
User user = users.get(position);
|
||||
String displayName =
|
||||
(user.getFullName() != null && !user.getFullName().isEmpty())
|
||||
? user.getFullName() + " (" + user.getLogin() + ")"
|
||||
: user.getLogin();
|
||||
holder.username.setText(displayName);
|
||||
|
||||
Glide.with(context)
|
||||
.load(user.getAvatarUrl())
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.loader_animated)
|
||||
.centerCrop()
|
||||
.into(holder.avatar);
|
||||
|
||||
holder.itemView.setBackground(null);
|
||||
holder.itemView.setOnClickListener(v -> listener.onUserClick(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return users.size();
|
||||
}
|
||||
|
||||
static class MentionViewHolder extends RecyclerView.ViewHolder {
|
||||
ImageView avatar;
|
||||
TextView username;
|
||||
|
||||
MentionViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
avatar = itemView.findViewById(R.id.avatar);
|
||||
username = itemView.findViewById(R.id.username);
|
||||
}
|
||||
}
|
||||
|
||||
interface OnUserClickListener {
|
||||
void onUserClick(User user);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/src/main/res/layout/list_users_mention.xml
Normal file
35
app/src/main/res/layout/list_users_mention.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/dimen48dp"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="@dimen/dimen32dp"
|
||||
android:layout_height="@dimen/dimen32dp"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
android:layout_gravity="center"
|
||||
app:cardElevation="@dimen/dimen0dp"
|
||||
app:cardCornerRadius="@dimen/dimen12dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/userAvatar"
|
||||
android:src="@drawable/ic_person"/>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="@dimen/dimen12dp"
|
||||
android:textColor="?attr/primaryTextColor"
|
||||
android:textSize="@dimen/dimen14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user