diff --git a/mastodon/build.gradle b/mastodon/build.gradle index d41aa042e..5e49a1512 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -32,7 +32,8 @@ dependencies { implementation 'me.grishka.litex:recyclerview:1.2.1' implementation 'me.grishka.litex:swiperefreshlayout:1.1.0' implementation 'me.grishka.litex:browser:1.4.0' - implementation 'me.grishka.appkit:appkit:1.0' + implementation 'me.grishka.appkit:appkit:1.1' implementation 'com.google.code.gson:gson:2.8.9' + implementation 'org.jsoup:jsoup:1.14.3' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' } \ No newline at end of file diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 5a9324cd9..5a6bbaf2c 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/Theme.Mastodon"> + android:theme="@style/Theme.Mastodon" + android:largeHeap="true"> diff --git a/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java b/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java index af439943d..c4ccfaf4d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java +++ b/mastodon/src/main/java/org/joinmastodon/android/MastodonApp.java @@ -4,6 +4,9 @@ import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; +import me.grishka.appkit.imageloader.ImageCache; +import me.grishka.appkit.utils.NetworkUtils; + public class MastodonApp extends Application{ @SuppressLint("StaticFieldLeak") // it's not a leak @@ -12,6 +15,11 @@ public class MastodonApp extends Application{ @Override public void onCreate(){ super.onCreate(); + ImageCache.Parameters params=new ImageCache.Parameters(); + params.diskCacheSize=100*1024*1024; + params.maxMemoryCacheSize=Integer.MAX_VALUE; + ImageCache.setParams(params); + NetworkUtils.setUserAgent("MastodonAndroid/"+BuildConfig.VERSION_NAME); context=getApplicationContext(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java index 02fd3d480..92e3357ee 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java @@ -60,6 +60,8 @@ public class MastodonAPIController{ public void submitRequest(final MastodonAPIRequest req){ thread.postRunnable(()->{ try{ + if(req.canceled) + return; Request.Builder builder=new Request.Builder() .url(req.getURL().toString()) .method(req.getMethod(), req.getRequestBody()) diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index ba1bbd696..1c87b1a7e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -32,6 +32,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ TypeToken respTypeToken; Call okhttpCall; Token token; + boolean canceled; public MastodonAPIRequest(HttpMethod method, String path, Class respClass){ this.path=path; @@ -47,6 +48,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ @Override public synchronized void cancel(){ + canceled=true; if(okhttpCall!=null){ okhttpCall.cancel(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHomeTimeline.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHomeTimeline.java new file mode 100644 index 000000000..c77edb5d9 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetHomeTimeline.java @@ -0,0 +1,20 @@ +package org.joinmastodon.android.api.requests.timelines; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +import java.util.List; + +public class GetHomeTimeline extends MastodonAPIRequest>{ + public GetHomeTimeline(String maxID, String minID, int limit){ + super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){}); + if(maxID!=null) + addQueryParameter("max_id", maxID); + if(minID!=null) + addQueryParameter("min_id", minID); + if(limit>0) + addQueryParameter("limit", ""+limit); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 2bee9447c..e19540b0c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -159,6 +159,7 @@ public class AccountSessionManager{ .build(); new CustomTabsIntent.Builder() + .setShareState(CustomTabsIntent.SHARE_STATE_OFF) .build() .launchUrl(context, uri); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 60c881ab6..7c3c934ea 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -4,15 +4,43 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import org.joinmastodon.android.R; import androidx.annotation.Nullable; import me.grishka.appkit.fragments.AppKitFragment; -import me.grishka.appkit.fragments.ToolbarFragment; +import me.grishka.appkit.views.FragmentRootLinearLayout; + +public class HomeFragment extends AppKitFragment{ + private FragmentRootLinearLayout content; + private HomeTimelineFragment homeTimelineFragment; + + private String accountID; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + } -public class HomeFragment extends ToolbarFragment{ @Nullable @Override - public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){ - return new View(getActivity()); + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){ + content=new FragmentRootLinearLayout(getActivity()); + content.setOrientation(LinearLayout.VERTICAL); + + FrameLayout fragmentContainer=new FrameLayout(getActivity()); + fragmentContainer.setId(R.id.fragment_wrap); + content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); + + Bundle args=new Bundle(); + args.putString("account", accountID); + homeTimelineFragment=new HomeTimelineFragment(); + homeTimelineFragment.setArguments(args); + getChildFragmentManager().beginTransaction().add(R.id.fragment_wrap, homeTimelineFragment).commit(); + + return content; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java new file mode 100644 index 000000000..ec677007c --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -0,0 +1,35 @@ +package org.joinmastodon.android.fragments; + +import android.app.Activity; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; +import org.joinmastodon.android.model.Status; + +import java.util.List; + +import me.grishka.appkit.api.SimpleCallback; + +public class HomeTimelineFragment extends StatusListFragment{ + private String accountID; + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + setTitle(R.string.app_name); + accountID=getArguments().getString("account"); + loadData(); + } + + @Override + protected void doLoadData(int offset, int count){ + new GetHomeTimeline(null, null, count) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + onDataLoaded(result, false); + } + }) + .exec(accountID); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java new file mode 100644 index 000000000..cc5f40004 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -0,0 +1,83 @@ +package org.joinmastodon.android.fragments; + +import android.view.ViewGroup; + +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.fragments.BaseRecyclerFragment; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.views.UsableRecyclerView; + +public abstract class StatusListFragment extends BaseRecyclerFragment{ + protected ArrayList displayItems=new ArrayList<>(); + + public StatusListFragment(){ + super(20); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + return new DisplayItemsAdapter(); + } + + @Override + public void onAppendItems(List items){ + super.onAppendItems(items); + for(Status s:items){ + displayItems.addAll(StatusDisplayItem.buildItems(this, s)); + } + } + + @Override + public void onClearItems(){ + super.onClearItems(); + displayItems.clear(); + } + + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ + + public DisplayItemsAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public BindableViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return (BindableViewHolder) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType], getActivity(), parent); + } + + @Override + public void onBindViewHolder(BindableViewHolder holder, int position){ + holder.bind(displayItems.get(position)); + super.onBindViewHolder(holder, position); + } + + @Override + public int getItemCount(){ + return displayItems.size(); + } + + @Override + public int getItemViewType(int position){ + return displayItems.get(position).getType().ordinal(); + } + + @Override + public int getImageCountForItem(int position){ + return displayItems.get(position).getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + return displayItems.get(position).getImageRequest(image); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index 5fca07777..a4423aa96 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -127,7 +127,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragmentg.activeUsers)).forEachOrdered(ig->sortedList.addAll(ig.instances)); + }).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances)); onDataLoaded(sortedList, false); updateFilteredList(); } @@ -299,18 +299,13 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment history; + + @Override + public String toString(){ + return "Hashtag{"+ + "name='"+name+'\''+ + ", url='"+url+'\''+ + ", history="+history+ + '}'; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/History.java b/mastodon/src/main/java/org/joinmastodon/android/model/History.java new file mode 100644 index 000000000..30f692a5c --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/History.java @@ -0,0 +1,19 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.AllFieldsAreRequired; + +@AllFieldsAreRequired +public class History extends BaseModel{ + public long day; // unixtime + public int uses; + public int accounts; + + @Override + public String toString(){ + return "History{"+ + "day="+day+ + ", uses="+uses+ + ", accounts="+accounts+ + '}'; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Mention.java b/mastodon/src/main/java/org/joinmastodon/android/model/Mention.java new file mode 100644 index 000000000..c64299e67 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Mention.java @@ -0,0 +1,21 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.AllFieldsAreRequired; + +@AllFieldsAreRequired +public class Mention extends BaseModel{ + public String id; + public String username; + public String acct; + public String url; + + @Override + public String toString(){ + return "Mention{"+ + "id='"+id+'\''+ + ", username='"+username+'\''+ + ", acct='"+acct+'\''+ + ", url='"+url+'\''+ + '}'; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java new file mode 100644 index 000000000..f5a252c17 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java @@ -0,0 +1,56 @@ +package org.joinmastodon.android.model; + +import org.joinmastodon.android.api.AllFieldsAreRequired; +import org.joinmastodon.android.api.ObjectValidationException; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +@AllFieldsAreRequired +public class Poll extends BaseModel{ + public String id; + public Instant expiresAt; + public boolean expired; + public boolean multiple; + public int votersCount; + public boolean voted; + public int[] ownVotes; + public List