add suggestions

This commit is contained in:
Thomas 2022-11-21 19:06:03 +01:00
parent 0796ff7b69
commit 020d218df9
11 changed files with 425 additions and 115 deletions

View File

@ -117,6 +117,7 @@ import app.fedilab.android.activities.ReorderTimelinesActivity;
import app.fedilab.android.activities.ScheduledActivity;
import app.fedilab.android.activities.SearchResultTabActivity;
import app.fedilab.android.activities.SettingsActivity;
import app.fedilab.android.activities.SuggestionActivity;
import app.fedilab.android.activities.TrendsActivity;
import app.fedilab.android.broadcastreceiver.NetworkStateReceiver;
import app.fedilab.android.client.entities.api.Emoji;
@ -391,6 +392,9 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
} else if (id == R.id.nav_trends) {
Intent intent = new Intent(this, TrendsActivity.class);
startActivity(intent);
} else if (id == R.id.nav_suggestions) {
Intent intent = new Intent(this, SuggestionActivity.class);
startActivity(intent);
} else if (id == R.id.nav_cache) {
Intent intent = new Intent(BaseMainActivity.this, CacheActivity.class);
startActivity(intent);

View File

@ -23,11 +23,10 @@ import androidx.core.content.ContextCompat;
import org.jetbrains.annotations.NotNull;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.databinding.ActivitySuggestionsBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount;
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonSuggestion;
public class SuggestionActivity extends BaseActivity {
@ -37,7 +36,7 @@ public class SuggestionActivity extends BaseActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.applyThemeBar(this);
app.fedilab.android.databinding.ActivitySuggestionsBinding binding = ActivitySuggestionsBinding.inflate(getLayoutInflater());
ActivitySuggestionsBinding binding = ActivitySuggestionsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
@ -46,9 +45,7 @@ public class SuggestionActivity extends BaseActivity {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
}
Bundle bundle = new Bundle();
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.ACCOUNT_SUGGESTION);
Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_tags, new FragmentMastodonAccount(), bundle, null, null);
Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_suggestions, new FragmentMastodonSuggestion(), null, null, null);
}

View File

@ -0,0 +1,140 @@
package app.fedilab.android.ui.drawer;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.recyclerview.widget.RecyclerView;
import java.lang.ref.WeakReference;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.activities.ProfileActivity;
import app.fedilab.android.client.entities.api.Account;
import app.fedilab.android.client.entities.api.Suggestion;
import app.fedilab.android.databinding.DrawerSuggestionBinding;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
public class SuggestionAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<Suggestion> suggestionList;
private Context context;
public SuggestionAdapter(List<Suggestion> suggestionList) {
this.suggestionList = suggestionList;
}
public int getCount() {
return suggestionList.size();
}
public Suggestion getItem(int position) {
return suggestionList.get(position);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
this.context = parent.getContext();
DrawerSuggestionBinding itemBinding = DrawerSuggestionBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new SuggestionViewHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
Account account = suggestionList.get(position).account;
SuggestionViewHolder holder = (SuggestionViewHolder) viewHolder;
MastodonHelper.loadPPMastodon(holder.binding.avatar, account);
AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class);
holder.binding.avatar.setOnClickListener(v -> {
Intent intent = new Intent(context, ProfileActivity.class);
Bundle b = new Bundle();
b.putSerializable(Helper.ARG_ACCOUNT, account);
intent.putExtras(b);
ActivityOptionsCompat options = ActivityOptionsCompat
.makeSceneTransitionAnimation((Activity) context, holder.binding.avatar, context.getString(R.string.activity_porfile_pp));
// start the new activity
context.startActivity(intent, options.toBundle());
});
holder.binding.followAction.setIconResource(R.drawable.ic_baseline_person_add_24);
holder.binding.followAction.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(context, R.color.cyanea_accent_dark_reference)));
holder.binding.displayName.setText(
account.getSpanDisplayName(context,
new WeakReference<>(holder.binding.displayName)),
TextView.BufferType.SPANNABLE);
holder.binding.username.setText(String.format("@%s", account.acct));
holder.binding.bio.setText(
account.getSpanNote(context,
new WeakReference<>(holder.binding.bio)),
TextView.BufferType.SPANNABLE);
holder.binding.followAction.setEnabled(false);
holder.binding.followAction.setOnClickListener(v -> {
suggestionList.remove(position);
notifyItemRemoved(position);
accountsVM.follow(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id, true, false);
});
holder.binding.notInterested.setOnClickListener(view -> {
suggestionList.remove(position);
notifyItemRemoved(position);
accountsVM.removeSuggestion(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id);
});
//TODO, remove when supported
holder.binding.notInterested.setVisibility(View.GONE);
}
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return suggestionList.size();
}
public static class SuggestionViewHolder extends RecyclerView.ViewHolder {
DrawerSuggestionBinding binding;
SuggestionViewHolder(DrawerSuggestionBinding itemView) {
super(itemView.getRoot());
binding = itemView;
}
}
}

View File

@ -143,14 +143,6 @@ public class FragmentMastodonAccount extends Fragment {
accountsVM.getBlocks(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), max_id, null)
.observe(getViewLifecycleOwner(), this::initializeAccountCommonView);
}
} else if (timelineType == Timeline.TimeLineEnum.ACCOUNT_SUGGESTION) {
if (firstLoad) {
accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
.observe(getViewLifecycleOwner(), this::initializeAccountCommonView);
} else {
accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id)
.observe(getViewLifecycleOwner(), this::initializeAccountCommonView);
}
}
}

View File

@ -0,0 +1,161 @@
package app.fedilab.android.ui.fragment.timeline;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R;
import app.fedilab.android.client.entities.api.Suggestion;
import app.fedilab.android.client.entities.api.Suggestions;
import app.fedilab.android.databinding.FragmentPaginationBinding;
import app.fedilab.android.helper.ThemeHelper;
import app.fedilab.android.ui.drawer.SuggestionAdapter;
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
public class FragmentMastodonSuggestion extends Fragment {
private FragmentPaginationBinding binding;
private AccountsVM accountsVM;
private boolean flagLoading;
private List<Suggestion> suggestions;
private String max_id;
private SuggestionAdapter suggestionAdapter;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
flagLoading = false;
binding = FragmentPaginationBinding.inflate(inflater, container, false);
binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity()));
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
int c1 = getResources().getColor(R.color.cyanea_accent_reference);
binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference));
binding.swipeContainer.setColorSchemeColors(
c1, c1, c1
);
binding.loader.setVisibility(View.VISIBLE);
binding.recyclerView.setVisibility(View.GONE);
accountsVM = new ViewModelProvider(FragmentMastodonSuggestion.this).get(AccountsVM.class);
max_id = null;
accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
.observe(getViewLifecycleOwner(), this::initializeAccountCommonView);
}
public void scrollToTop() {
binding.recyclerView.setAdapter(suggestionAdapter);
}
/**
* Intialize the view for Suggestions
*
* @param suggestions {@link Suggestions}
*/
private void initializeAccountCommonView(final Suggestions suggestions) {
flagLoading = false;
if (binding == null || !isAdded() || getActivity() == null) {
return;
}
binding.loader.setVisibility(View.GONE);
binding.noAction.setVisibility(View.GONE);
binding.swipeContainer.setRefreshing(false);
binding.swipeContainer.setOnRefreshListener(() -> {
binding.swipeContainer.setRefreshing(true);
flagLoading = false;
max_id = null;
accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null)
.observe(getViewLifecycleOwner(), this::initializeAccountCommonView);
});
if (suggestions == null || suggestions.suggestions == null || suggestions.suggestions.size() == 0) {
binding.noAction.setVisibility(View.VISIBLE);
binding.noActionText.setText(R.string.no_accounts);
return;
}
binding.recyclerView.setVisibility(View.VISIBLE);
this.suggestions = suggestions.suggestions;
suggestionAdapter = new SuggestionAdapter(this.suggestions);
flagLoading = suggestions.pagination.max_id == null;
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
binding.recyclerView.setLayoutManager(mLayoutManager);
binding.recyclerView.setAdapter(suggestionAdapter);
max_id = suggestions.pagination.max_id;
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
if (dy > 0) {
int visibleItemCount = mLayoutManager.getChildCount();
int totalItemCount = mLayoutManager.getItemCount();
if (firstVisibleItem + visibleItemCount == totalItemCount) {
if (!flagLoading) {
flagLoading = true;
binding.loadingNextElements.setVisibility(View.VISIBLE);
accountsVM.getSuggestions(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id)
.observe(getViewLifecycleOwner(), suggestionsPaginated -> {
dealWithPagination(suggestionsPaginated);
});
}
} else {
binding.loadingNextElements.setVisibility(View.GONE);
}
}
}
});
}
/**
* Update view and pagination when scrolling down
*
* @param suggestions_fetched Suggestions
*/
private void dealWithPagination(Suggestions suggestions_fetched) {
flagLoading = false;
if (binding == null || !isAdded() || getActivity() == null) {
return;
}
binding.loadingNextElements.setVisibility(View.GONE);
if (this.suggestions != null && suggestions_fetched != null && suggestions_fetched.suggestions != null) {
flagLoading = suggestions_fetched.pagination.max_id == null;
int startId = this.suggestions.size();
this.suggestions.addAll(suggestions_fetched.suggestions);
max_id = suggestions_fetched.pagination.max_id;
suggestionAdapter.notifyItemRangeInserted(startId, suggestions_fetched.suggestions.size());
} else {
flagLoading = true;
}
}
}

View File

@ -1,98 +0,0 @@
package app.fedilab.android.viewmodel.mastodon;
/* Copyright 2022 Thomas Schneider
*
* This file is a part of Fedilab
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.util.List;
import java.util.concurrent.TimeUnit;
import app.fedilab.android.client.endpoints.MastodonFiltersService;
import app.fedilab.android.client.endpoints.MastodonTimelinesService;
import app.fedilab.android.client.entities.api.Filter;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.api.Statuses;
import app.fedilab.android.client.entities.app.Timeline;
import app.fedilab.android.helper.Helper;
import app.fedilab.android.helper.MastodonHelper;
import app.fedilab.android.helper.TimelineHelper;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class SuggestionVM extends AndroidViewModel {
final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(60, TimeUnit.SECONDS)
.connectTimeout(60, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS)
.proxy(Helper.getProxy(getApplication().getApplicationContext()))
.build();
private MutableLiveData<Filter> filterMutableLiveData;
private MutableLiveData<List<Filter>> filterListMutableLiveData;
public SuggestionVM(@NonNull Application application) {
super(application);
}
private MastodonFiltersService initV2(String instance) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + instance + "/api/v2/")
// .addConverterFactory(GsonConverterFactory.create(Helper.getDateBuilder()))
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
return retrofit.create(MastodonFiltersService.class);
}
public LiveData<Suggestions> getSuggestions(String token, @NonNull String instance, String max_id, Integer limit) {
MastodonTimelinesService mastodonTimelinesService = initV2(instance);
statusesMutableLiveData = new MutableLiveData<>();
new Thread(() -> {
Call<List<Status>> publicTlCall = mastodonTimelinesService.getStatusTrends(token, max_id, limit);
Statuses statuses = new Statuses();
if (publicTlCall != null) {
try {
Response<List<Status>> publicTlResponse = publicTlCall.execute();
if (publicTlResponse.isSuccessful()) {
List<Status> statusList = publicTlResponse.body();
statuses.statuses = TimelineHelper.filterStatus(getApplication().getApplicationContext(), statusList, Timeline.TimeLineEnum.TREND_MESSAGE);
statuses.pagination = MastodonHelper.getOffSetPagination(publicTlResponse.headers());
}
} catch (Exception e) {
e.printStackTrace();
}
}
Handler mainHandler = new Handler(Looper.getMainLooper());
Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses);
mainHandler.post(myRunnable);
}).start();
return statusesMutableLiveData;
}
}

View File

@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path

View File

@ -22,7 +22,7 @@
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_accounts"
android:id="@+id/nav_host_fragment_suggestions"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2021 Thomas Schneider
This file is a part of Fedilab
This program is free software; you can redistribute it and/or modify it under the terms of the
GNU General Public License as published by the Free Software Foundation; either version 3 of the
License, or (at your option) any later version.
Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details.
You should have received a copy of the GNU General Public License along with Fedilab; if not,
see <http://www.gnu.org/licenses>
-->
<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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6dp"
android:layout_marginTop="6dp"
android:backgroundTint="@color/cyanea_primary_dark_reference"
app:cardElevation="0dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/avatar"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_person" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/display_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@tools:sample/full_names" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:ellipsize="end"
android:singleLine="true"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/display_name"
tools:text="\@username@instance.test" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintTop_toBottomOf="@id/avatar"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/not_interested"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="6dp"
android:text="@string/not_interested"
android:textColor="@color/cyanea_accent_dark_reference"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bio"
app:strokeColor="@color/cyanea_accent_dark_reference" />
<com.google.android.material.button.MaterialButton
android:id="@+id/follow_action"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:padding="6dp"
app:icon="@drawable/ic_baseline_person_add_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bio" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -58,7 +58,11 @@
android:icon="@drawable/ic_baseline_trending_up_24"
android:title="@string/trending"
android:visible="true" />
<item
android:id="@+id/nav_suggestions"
android:icon="@drawable/ic_baseline_account_circle_24"
android:title="@string/Suggestions"
android:visible="true" />
<item
android:id="@+id/nav_about_instance"
android:icon="@drawable/ic_info_outline_white_24dp"

View File

@ -1905,4 +1905,5 @@
<string name="notif_submitted_report">Submitted a report</string>
<string name="notif_signed_up">Signed up</string>
<string name="Suggestions">Suggestions</string>
<string name="not_interested">Not interested</string>
</resources>