Add CI statuses checks to PR, improve reactions UI

This commit is contained in:
M M Arif
2026-04-22 14:34:20 +05:00
parent f8585509e5
commit d359e98f75
9 changed files with 265 additions and 77 deletions

View File

@@ -14,6 +14,7 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.chip.ChipGroup;
@@ -24,12 +25,14 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import org.gitnex.tea4j.v2.models.CommitStatus;
import org.gitnex.tea4j.v2.models.Issue;
import org.gitnex.tea4j.v2.models.Label;
import org.gitnex.tea4j.v2.models.PullRequest;
import org.gitnex.tea4j.v2.models.Repository;
import org.gitnex.tea4j.v2.models.User;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.CommitStatusesAdapter;
import org.mian.gitnex.databinding.ActivityPullRequestDetailsBinding;
import org.mian.gitnex.databinding.ItemPrMetaRowBinding;
import org.mian.gitnex.databinding.LayoutPrHeaderBinding;
@@ -41,6 +44,7 @@ import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.UIHelper;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
import org.mian.gitnex.viewmodels.CommitStatusesViewModel;
import org.mian.gitnex.viewmodels.PullRequestDetailsViewModel;
import org.mian.gitnex.viewmodels.ReactionsViewModel;
import org.mian.gitnex.views.reactions.ReactionUsersBottomSheet;
@@ -54,6 +58,7 @@ public class PullRequestDetailsActivity extends BaseActivity {
private ActivityPullRequestDetailsBinding binding;
private PullRequestDetailsViewModel viewModel;
private ReactionsViewModel reactionsViewModel;
private CommitStatusesViewModel statusesViewModel;
private ReactionsManager reactionsManager;
private String owner;
private String repo;
@@ -71,6 +76,7 @@ public class PullRequestDetailsActivity extends BaseActivity {
viewModel = new ViewModelProvider(this).get(PullRequestDetailsViewModel.class);
reactionsViewModel = new ViewModelProvider(this).get(ReactionsViewModel.class);
statusesViewModel = new ViewModelProvider(this).get(CommitStatusesViewModel.class);
UIHelper.applyEdgeToEdge(
this,
@@ -113,6 +119,7 @@ public class PullRequestDetailsActivity extends BaseActivity {
setupListeners();
observeViewModel();
observeReactionsViewModel();
observeStatusesViewModel();
fetchPullRequestData();
fetchReactionSettings();
}
@@ -439,11 +446,6 @@ public class PullRequestDetailsActivity extends BaseActivity {
new RepositoryContext(owner, repo, this));
}
private void populateChecks(PullRequest pr) {
// TODO: Fetch and populate CI checks status
binding.checksCard.getRoot().setVisibility(View.GONE);
}
private void setupMetaRow(ItemPrMetaRowBinding row, int iconRes, String text) {
row.metaIcon.setImageResource(iconRes);
row.metaText.setText(text);
@@ -544,6 +546,77 @@ public class PullRequestDetailsActivity extends BaseActivity {
});
}
private void observeStatusesViewModel() {
statusesViewModel
.getStatuses()
.observe(
this,
statuses -> {
if (statuses != null && !statuses.isEmpty()) {
binding.checksCard.getRoot().setVisibility(View.VISIBLE);
setupChecksList(statuses);
} else {
binding.checksCard.getRoot().setVisibility(View.GONE);
}
});
statusesViewModel
.getHasStatuses()
.observe(
this,
hasStatuses -> {
if (!hasStatuses) {
binding.checksCard.getRoot().setVisibility(View.GONE);
}
});
statusesViewModel.getIsLoading().observe(this, loading -> {});
statusesViewModel
.getError()
.observe(
this,
error -> {
if (error != null) {
binding.checksCard.getRoot().setVisibility(View.GONE);
statusesViewModel.clearError();
}
});
}
private void fetchStatuses(String sha) {
if (owner != null && repo != null && sha != null) {
statusesViewModel.fetchStatuses(this, owner, repo, sha);
}
}
private void setupChecksList(List<CommitStatus> statuses) {
binding.checksCard.checksList.setLayoutManager(new LinearLayoutManager(this));
binding.checksCard.checksList.setAdapter(new CommitStatusesAdapter(statuses));
long successCount =
statuses.stream()
.filter(s -> "success".equalsIgnoreCase(s.getStatus().toString()))
.count();
long totalCount = statuses.size();
String summary;
if (successCount == totalCount) {
summary = getString(R.string.checks_all_passed, totalCount);
} else {
summary = getString(R.string.checks_summary, successCount, totalCount);
}
binding.checksCard.checksSummary.setText(summary);
}
private void populateChecks(PullRequest pr) {
if (pr.getHead() != null && pr.getHead().getSha() != null) {
fetchStatuses(pr.getHead().getSha());
} else {
binding.checksCard.getRoot().setVisibility(View.GONE);
}
}
private String formatDate(Date date) {
if (date == null) return "";
SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());

View File

@@ -0,0 +1,110 @@
package org.mian.gitnex.viewmodels;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.gitnex.tea4j.v2.models.CommitStatus;
import org.mian.gitnex.R;
import org.mian.gitnex.clients.RetrofitClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* @author mmarif
*/
public class CommitStatusesViewModel extends ViewModel {
private final MutableLiveData<List<CommitStatus>> statuses = new MutableLiveData<>();
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
private final MutableLiveData<String> error = new MutableLiveData<>();
private final MutableLiveData<Boolean> hasStatuses = new MutableLiveData<>(false);
public LiveData<List<CommitStatus>> getStatuses() {
return statuses;
}
public LiveData<Boolean> getIsLoading() {
return isLoading;
}
public LiveData<String> getError() {
return error;
}
public LiveData<Boolean> getHasStatuses() {
return hasStatuses;
}
public void clearError() {
error.setValue(null);
}
public void fetchStatuses(Context ctx, String owner, String repo, String sha) {
isLoading.setValue(true);
error.setValue(null);
Call<List<CommitStatus>> call =
RetrofitClient.getApiInterface(ctx)
.repoListStatuses(owner, repo, sha, null, null, null, null);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<CommitStatus>> call,
@NonNull Response<List<CommitStatus>> response) {
isLoading.setValue(false);
if (response.isSuccessful() && response.body() != null) {
if (response.body().isEmpty()) {
hasStatuses.setValue(false);
statuses.setValue(new ArrayList<>());
return;
}
ArrayList<CommitStatus> merged = new ArrayList<>();
for (CommitStatus c : response.body()) {
boolean exists = false;
for (int i = 0; i < merged.size(); i++) {
if (Objects.equals(
merged.get(i).getContext(), c.getContext())) {
if (merged.get(i).getCreatedAt() != null
&& c.getCreatedAt() != null
&& merged.get(i)
.getCreatedAt()
.before(c.getCreatedAt())) {
merged.set(i, c);
}
exists = true;
break;
}
}
if (!exists) {
merged.add(c);
}
}
hasStatuses.setValue(!merged.isEmpty());
statuses.setValue(merged);
} else {
hasStatuses.setValue(false);
statuses.setValue(new ArrayList<>());
}
}
@Override
public void onFailure(
@NonNull Call<List<CommitStatus>> call, @NonNull Throwable t) {
isLoading.setValue(false);
hasStatuses.setValue(false);
error.setValue(ctx.getString(R.string.genericError));
}
});
}
}

View File

@@ -35,15 +35,25 @@ public class EmojiPickerPopup extends PopupWindow {
PopupEmojiPickerBinding.inflate(LayoutInflater.from(context));
setContentView(binding.getRoot());
int columns = 4;
int itemCount = allowedReactions.size();
int columns = (itemCount <= 10) ? 5 : 6;
int rows = (int) Math.ceil((double) itemCount / columns);
int itemSize = (int) (48 * context.getResources().getDisplayMetrics().density);
int gridPadding = (int) (8 * context.getResources().getDisplayMetrics().density);
float density = context.getResources().getDisplayMetrics().density;
int itemSlotSize = (int) (48 * density);
int gridPadding = (int) (8 * density);
int width = columns * itemSize + (gridPadding * 2);
int height = rows * itemSize + (gridPadding * 2);
int maxRowsToShow = 3;
int displayRows = Math.min(rows, maxRowsToShow);
int width = (columns * itemSlotSize) + (gridPadding * 2);
int height = (displayRows * itemSlotSize) + (gridPadding * 2);
if (rows > maxRowsToShow) {
binding.emojiGrid.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
} else {
binding.emojiGrid.setOverScrollMode(View.OVER_SCROLL_NEVER);
}
setWidth(width);
setHeight(height);
@@ -56,6 +66,8 @@ public class EmojiPickerPopup extends PopupWindow {
binding.emojiGrid.setLayoutManager(new GridLayoutManager(context, columns));
binding.emojiGrid.setPadding(gridPadding, gridPadding, gridPadding, gridPadding);
binding.emojiGrid.setOverScrollMode(View.OVER_SCROLL_NEVER);
EmojiPickerAdapter adapter =
new EmojiPickerAdapter(context, allowedReactions, new ArrayList<>());
adapter.setOnEmojiClickListener(

View File

@@ -4,42 +4,28 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20.985,12.528a9,9 0,1 0,-8.45 8.456"
android:pathData="M3,12a9,9 0,1 0,18 0a9,9 0,1 0,-18 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M16,19h6"
android:pathData="M9,10l0.01,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M19,16v6"
android:pathData="M15,10l0.01,0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M9,10h0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M15,10h0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M9.5,15c0.658,0.64 1.56,1 2.5,1s1.842,-0.36 2.5,-1"
android:pathData="M9.5,15a3.5,3.5 0,0 0,5 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_width="@dimen/dimen40dp"
android:layout_height="@dimen/dimen40dp"
android:layout_margin="@dimen/dimen4dp"
app:cardBackgroundColor="@android:color/transparent"
app:cardElevation="@dimen/dimen0dp"
@@ -22,7 +22,7 @@
android:id="@+id/emojiText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="@dimen/dimen18sp"
android:textSize="@dimen/dimen20sp"
android:gravity="center" />
<TextView

View File

@@ -1,33 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.CardView.Elevated"
<com.google.android.material.card.MaterialCardView style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dimen16dp"
android:layout_marginTop="@dimen/dimen16dp"
android:layout_marginHorizontal="@dimen/dimen12dp"
app:cardBackgroundColor="?attr/materialCardBackgroundColor"
app:cardElevation="@dimen/dimen0dp"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Large"
app:strokeWidth="@dimen/dimen0dp">
app:cardCornerRadius="@dimen/dimen24dp"
app:cardElevation="0dp"
app:strokeWidth="0dp"
android:layout_marginTop="@dimen/dimen16dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/dimen16dp"
android:orientation="vertical">
<LinearLayout
android:id="@+id/checksHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="@dimen/dimen16dp">
android:padding="@dimen/dimen20dp"
tools:ignore="UseCompoundDrawables">
<TextView
android:id="@+id/checksSummary"
@@ -35,8 +34,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
tools:text="✅ 3/3 checks passed" />
android:textColor="?attr/colorOnSurface" />
<ImageView
android:id="@+id/checksExpandIcon"
@@ -57,12 +55,6 @@
android:paddingBottom="@dimen/dimen16dp"
android:visibility="gone">
<View
android:layout_width="match_parent"
android:layout_height="@dimen/dimen1dp"
android:layout_marginBottom="@dimen/dimen12dp"
android:background="?attr/colorOutlineVariant" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/checksList"
android:layout_width="match_parent"

View File

@@ -1,43 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Elevated"
<com.google.android.material.card.MaterialCardView style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dimen16dp"
android:layout_marginTop="@dimen/dimen16dp"
android:layout_marginHorizontal="@dimen/dimen12dp"
app:cardBackgroundColor="?attr/materialCardBackgroundColor"
app:cardElevation="@dimen/dimen0dp"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Large"
app:strokeWidth="@dimen/dimen0dp">
app:cardCornerRadius="@dimen/dimen24dp"
app:cardElevation="0dp"
app:strokeWidth="0dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/dimen20dp">
<View
android:id="@+id/desc_accent"
android:layout_width="@dimen/dimen4dp"
android:layout_height="0dp"
android:background="?attr/iconsColor"
app:layout_constraintBottom_toBottomOf="@+id/desc_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/desc_title" />
<TextView
android:id="@+id/desc_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen8dp"
android:layout_marginStart="@dimen/dimen12dp"
android:text="@string/description"
android:textAppearance="?attr/textAppearanceLabelLarge"
android:textColor="?attr/colorPrimary" />
android:textAppearance="?attr/textAppearanceBodyLarge"
android:textColor="?attr/colorOnSurface"
app:layout_constraintStart_toEndOf="@id/desc_accent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/descriptionContent"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
android:layout_marginTop="@dimen/dimen16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/desc_title" />
<LinearLayout
android:id="@+id/attachmentsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/descriptionContent"
android:orientation="vertical" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -7,7 +7,7 @@
android:background="?attr/materialCardBackgroundColor"
android:paddingHorizontal="@dimen/dimen16dp"
android:paddingTop="@dimen/dimen24dp"
android:paddingBottom="@dimen/dimen24dp">
android:paddingBottom="@dimen/dimen12dp">
<ImageView
android:id="@+id/status_badge_img"
@@ -152,15 +152,11 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/add_reaction_button"
style="@style/Widget.Material3.Button.TextButton"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/addButton"
android:textAllCaps="false"
app:icon="@drawable/ic_emoji_add"
app:iconGravity="textStart"
app:iconPadding="@dimen/dimen4dp"
app:iconSize="@dimen/dimen18dp" />
app:iconSize="@dimen/dimen24dp" />
</LinearLayout>

View File

@@ -1229,6 +1229,8 @@
<string name="invalid_version_format">Invalid version format. Please use format like 1.26.1</string>
<string name="invalid_pr">Invalid pull request</string>
<string name="reactions_by">%s Reactions</string>
<string name="checks_all_passed">✅ All %d checks passed</string>
<string name="checks_summary">%d/%d checks passed</string>
<string name="time_in_moments">in moments</string>
<string name="time_in_minute">in %d minute</string>