diff --git a/app/src/main/java/org/mian/gitnex/activities/PullRequestDetailsActivity.java b/app/src/main/java/org/mian/gitnex/activities/PullRequestDetailsActivity.java index a5631377..417e176c 100644 --- a/app/src/main/java/org/mian/gitnex/activities/PullRequestDetailsActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/PullRequestDetailsActivity.java @@ -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 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()); diff --git a/app/src/main/java/org/mian/gitnex/viewmodels/CommitStatusesViewModel.java b/app/src/main/java/org/mian/gitnex/viewmodels/CommitStatusesViewModel.java new file mode 100644 index 00000000..6e2da4ce --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/viewmodels/CommitStatusesViewModel.java @@ -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> statuses = new MutableLiveData<>(); + private final MutableLiveData isLoading = new MutableLiveData<>(false); + private final MutableLiveData error = new MutableLiveData<>(); + private final MutableLiveData hasStatuses = new MutableLiveData<>(false); + + public LiveData> getStatuses() { + return statuses; + } + + public LiveData getIsLoading() { + return isLoading; + } + + public LiveData getError() { + return error; + } + + public LiveData 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> call = + RetrofitClient.getApiInterface(ctx) + .repoListStatuses(owner, repo, sha, null, null, null, null); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull Response> response) { + isLoading.setValue(false); + + if (response.isSuccessful() && response.body() != null) { + if (response.body().isEmpty()) { + hasStatuses.setValue(false); + statuses.setValue(new ArrayList<>()); + return; + } + + ArrayList 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> call, @NonNull Throwable t) { + isLoading.setValue(false); + hasStatuses.setValue(false); + error.setValue(ctx.getString(R.string.genericError)); + } + }); + } +} diff --git a/app/src/main/java/org/mian/gitnex/views/reactions/EmojiPickerPopup.java b/app/src/main/java/org/mian/gitnex/views/reactions/EmojiPickerPopup.java index 7418b2db..3fd8619e 100644 --- a/app/src/main/java/org/mian/gitnex/views/reactions/EmojiPickerPopup.java +++ b/app/src/main/java/org/mian/gitnex/views/reactions/EmojiPickerPopup.java @@ -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( diff --git a/app/src/main/res/drawable/ic_emoji_add.xml b/app/src/main/res/drawable/ic_emoji_add.xml index 83c4ba9a..6232d5bf 100644 --- a/app/src/main/res/drawable/ic_emoji_add.xml +++ b/app/src/main/res/drawable/ic_emoji_add.xml @@ -4,42 +4,28 @@ android:viewportWidth="24" android:viewportHeight="24"> - - - + 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"> + android:padding="@dimen/dimen20dp" + tools:ignore="UseCompoundDrawables"> + android:textColor="?attr/colorOnSurface" /> - - - + 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"> - + + + + android:textAppearance="?attr/textAppearanceBodyLarge" + android:textColor="?attr/colorOnSurface" + app:layout_constraintStart_toEndOf="@id/desc_accent" + app:layout_constraintTop_toTopOf="parent" /> + android:layout_marginTop="@dimen/dimen16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/desc_title" /> - + diff --git a/app/src/main/res/layout/layout_pr_header.xml b/app/src/main/res/layout/layout_pr_header.xml index 37e803da..ed8f4c09 100644 --- a/app/src/main/res/layout/layout_pr_header.xml +++ b/app/src/main/res/layout/layout_pr_header.xml @@ -7,7 +7,7 @@ android:background="?attr/materialCardBackgroundColor" android:paddingHorizontal="@dimen/dimen16dp" android:paddingTop="@dimen/dimen24dp" - android:paddingBottom="@dimen/dimen24dp"> + android:paddingBottom="@dimen/dimen12dp"> + app:iconSize="@dimen/dimen24dp" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0209a09..f2781823 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1229,6 +1229,8 @@ Invalid version format. Please use format like 1.26.1 Invalid pull request %s Reactions + ✅ All %d checks passed + %d/%d checks passed in moments in %d minute