+ * The boundary callback might be called multiple times for the same direction so it does its own
+ * rate limiting using the PagingRequestHelper class.
+ */
+class ConversationsBoundaryCallback(
+ private val accountId: Long,
+ private val mastodonApi: MastodonApi,
+ private val handleResponse: (Long, List
+ * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
+ * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
+ * for each of them via {@link #runIfNotRunning(RequestType, Request)}.
+ *
+ * It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
+ *
+ * A sample usage of this class to limit requests looks like this:
+ *
+ * The helper provides an API to observe combined request status, which can be reported back to the
+ * application based on your business rules.
+ *
+ * If run, the request will be run in the current thread.
+ *
+ * @param type The type of the request.
+ * @param request The request to run.
+ * @return True if the request is run, false otherwise.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @AnyThread
+ public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
+ boolean hasListeners = !mListeners.isEmpty();
+ StatusReport report = null;
+ synchronized (mLock) {
+ RequestQueue queue = mRequestQueues[type.ordinal()];
+ if (queue.mRunning != null) {
+ return false;
+ }
+ queue.mRunning = request;
+ queue.mStatus = Status.RUNNING;
+ queue.mFailed = null;
+ queue.mLastError = null;
+ if (hasListeners) {
+ report = prepareStatusReportLocked();
+ }
+ }
+ if (report != null) {
+ dispatchReport(report);
+ }
+ final RequestWrapper wrapper = new RequestWrapper(request, this, type);
+ wrapper.run();
+ return true;
+ }
+ @GuardedBy("mLock")
+ private StatusReport prepareStatusReportLocked() {
+ Throwable[] errors = new Throwable[]{
+ mRequestQueues[0].mLastError,
+ mRequestQueues[1].mLastError,
+ mRequestQueues[2].mLastError
+ };
+ return new StatusReport(
+ getStatusForLocked(RequestType.INITIAL),
+ getStatusForLocked(RequestType.BEFORE),
+ getStatusForLocked(RequestType.AFTER),
+ errors
+ );
+ }
+ @GuardedBy("mLock")
+ private Status getStatusForLocked(RequestType type) {
+ return mRequestQueues[type.ordinal()].mStatus;
+ }
+ @AnyThread
+ @VisibleForTesting
+ void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
+ StatusReport report = null;
+ final boolean success = throwable == null;
+ boolean hasListeners = !mListeners.isEmpty();
+ synchronized (mLock) {
+ RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
+ queue.mRunning = null;
+ queue.mLastError = throwable;
+ if (success) {
+ queue.mFailed = null;
+ queue.mStatus = Status.SUCCESS;
+ } else {
+ queue.mFailed = wrapper;
+ queue.mStatus = Status.FAILED;
+ }
+ if (hasListeners) {
+ report = prepareStatusReportLocked();
+ }
+ }
+ if (report != null) {
+ dispatchReport(report);
+ }
+ }
+ private void dispatchReport(StatusReport report) {
+ for (Listener listener : mListeners) {
+ listener.onStatusChange(report);
+ }
+ }
+ /**
+ * Retries all failed requests.
+ *
+ * @return True if any request is retried, false otherwise.
+ */
+ public boolean retryAllFailed() {
+ final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
+ boolean retried = false;
+ synchronized (mLock) {
+ for (int i = 0; i < RequestType.values().length; i++) {
+ toBeRetried[i] = mRequestQueues[i].mFailed;
+ mRequestQueues[i].mFailed = null;
+ }
+ }
+ for (RequestWrapper failed : toBeRetried) {
+ if (failed != null) {
+ failed.retry(mRetryService);
+ retried = true;
+ }
+ }
+ return retried;
+ }
+ static class RequestWrapper implements Runnable {
+ @NonNull
+ final Request mRequest;
+ @NonNull
+ final PagingRequestHelper mHelper;
+ @NonNull
+ final RequestType mType;
+ RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
+ @NonNull RequestType type) {
+ mRequest = request;
+ mHelper = helper;
+ mType = type;
+ }
+ @Override
+ public void run() {
+ mRequest.run(new Request.Callback(this, mHelper));
+ }
+ void retry(Executor service) {
+ service.execute(new Runnable() {
+ @Override
+ public void run() {
+ mHelper.runIfNotRunning(mType, mRequest);
+ }
+ });
+ }
+ }
+ /**
+ * Runner class that runs a request tracked by the {@link PagingRequestHelper}.
+ *
+ * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
+ * or {@link Callback#recordSuccess()} once and only once. This call
+ * can be made any time. Until that method call is made, {@link PagingRequestHelper} will
+ * consider the request is running.
+ */
+ @FunctionalInterface
+ public interface Request {
+ /**
+ * Should run the request and call the given {@link Callback} with the result of the
+ * request.
+ *
+ * @param callback The callback that should be invoked with the result.
+ */
+ void run(Callback callback);
+ /**
+ * Callback class provided to the {@link #run(Callback)} method to report the result.
+ */
+ class Callback {
+ private final AtomicBoolean mCalled = new AtomicBoolean();
+ private final RequestWrapper mWrapper;
+ private final PagingRequestHelper mHelper;
+ Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
+ mWrapper = wrapper;
+ mHelper = helper;
+ }
+ /**
+ * Call this method when the request succeeds and new data is fetched.
+ */
+ @SuppressWarnings("unused")
+ public final void recordSuccess() {
+ if (mCalled.compareAndSet(false, true)) {
+ mHelper.recordResult(mWrapper, null);
+ } else {
+ throw new IllegalStateException(
+ "already called recordSuccess or recordFailure");
+ }
+ }
+ /**
+ * Call this method with the failure message and the request can be retried via
+ * {@link #retryAllFailed()}.
+ *
+ * @param throwable The error that occured while carrying out the request.
+ */
+ @SuppressWarnings("unused")
+ public final void recordFailure(@NonNull Throwable throwable) {
+ //noinspection ConstantConditions
+ if (throwable == null) {
+ throw new IllegalArgumentException("You must provide a throwable describing"
+ + " the error to record the failure");
+ }
+ if (mCalled.compareAndSet(false, true)) {
+ mHelper.recordResult(mWrapper, throwable);
+ } else {
+ throw new IllegalStateException(
+ "already called recordSuccess or recordFailure");
+ }
+ }
+ }
+ }
+ /**
+ * Data class that holds the information about the current status of the ongoing requests
+ * using this helper.
+ */
+ public static final class StatusReport {
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#INITIAL}.
+ */
+ @NonNull
+ public final Status initial;
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#BEFORE}.
+ */
+ @NonNull
+ public final Status before;
+ /**
+ * Status of the latest request that were submitted with {@link RequestType#AFTER}.
+ */
+ @NonNull
+ public final Status after;
+ @NonNull
+ private final Throwable[] mErrors;
+ StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
+ @NonNull Throwable[] errors) {
+ this.initial = initial;
+ this.before = before;
+ this.after = after;
+ this.mErrors = errors;
+ }
+ /**
+ * Convenience method to check if there are any running requests.
+ *
+ * @return True if there are any running requests, false otherwise.
+ */
+ public boolean hasRunning() {
+ return initial == Status.RUNNING
+ || before == Status.RUNNING
+ || after == Status.RUNNING;
+ }
+ /**
+ * Convenience method to check if there are any requests that resulted in an error.
+ *
+ * @return True if there are any requests that finished with error, false otherwise.
+ */
+ public boolean hasError() {
+ return initial == Status.FAILED
+ || before == Status.FAILED
+ || after == Status.FAILED;
+ }
+ /**
+ * Returns the error for the given request type.
+ *
+ * @param type The request type for which the error should be returned.
+ * @return The {@link Throwable} returned by the failing request with the given type or
+ * {@code null} if the request for the given type did not fail.
+ */
+ @Nullable
+ public Throwable getErrorFor(@NonNull RequestType type) {
+ return mErrors[type.ordinal()];
+ }
+ @Override
+ public String toString() {
+ return "StatusReport{"
+ + "initial=" + initial
+ + ", before=" + before
+ + ", after=" + after
+ + ", mErrors=" + Arrays.toString(mErrors)
+ + '}';
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ StatusReport that = (StatusReport) o;
+ if (initial != that.initial) return false;
+ if (before != that.before) return false;
+ if (after != that.after) return false;
+ // Probably incorrect - comparing Object[] arrays with Arrays.equals
+ return Arrays.equals(mErrors, that.mErrors);
+ }
+ @Override
+ public int hashCode() {
+ int result = initial.hashCode();
+ result = 31 * result + before.hashCode();
+ result = 31 * result + after.hashCode();
+ result = 31 * result + Arrays.hashCode(mErrors);
+ return result;
+ }
+ }
+ /**
+ * Listener interface to get notified by request status changes.
+ */
+ public interface Listener {
+ /**
+ * Called when the status for any of the requests has changed.
+ *
+ * @param report The current status report that has all the information about the requests.
+ */
+ void onStatusChange(@NonNull StatusReport report);
+ }
+ /**
+ * Represents the status of a Request for each {@link RequestType}.
+ */
+ public enum Status {
+ /**
+ * There is current a running request.
+ */
+ RUNNING,
+ /**
+ * The last request has succeeded or no such requests have ever been run.
+ */
+ SUCCESS,
+ /**
+ * The last request has failed.
+ */
+ FAILED
+ }
+ /**
+ * Available request types.
+ */
+ public enum RequestType {
+ /**
+ * Corresponds to an initial request made to a {@link DataSource} or the empty state for
+ * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ INITIAL,
+ /**
+ * Corresponds to the {@code loadBefore} calls in {@link DataSource} or
+ * {@code onItemAtFrontLoaded} in
+ * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ BEFORE,
+ /**
+ * Corresponds to the {@code loadAfter} calls in {@link DataSource} or
+ * {@code onItemAtEndLoaded} in
+ * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+ */
+ AFTER
+ }
+ class RequestQueue {
+ @NonNull
+ final RequestType mRequestType;
+ @Nullable
+ RequestWrapper mFailed;
+ @Nullable
+ Request mRunning;
+ @Nullable
+ Throwable mLastError;
+ @NonNull
+ Status mStatus = Status.SUCCESS;
+ RequestQueue(@NonNull RequestType requestType) {
+ mRequestType = requestType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java
index b1329cba9..2ba900e7d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java
@@ -25,7 +25,8 @@ import androidx.annotation.AttrRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
-import androidx.core.content.ContextCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import android.util.TypedValue;
import android.widget.ImageView;
@@ -37,12 +38,12 @@ import android.widget.ImageView;
public class ThemeUtils {
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
- public static final String THEME_NIGHT = "night";
- public static final String THEME_DAY = "day";
- public static final String THEME_BLACK = "black";
- public static final String THEME_AUTO = "auto";
+ private static final String THEME_NIGHT = "night";
+ private static final String THEME_DAY = "day";
+ private static final String THEME_BLACK = "black";
+ private static final String THEME_AUTO = "auto";
- public static Drawable getDrawable(Context context, @AttrRes int attribute,
+ public static Drawable getDrawable(@NonNull Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawable) {
TypedValue value = new TypedValue();
@DrawableRes int resourceId;
@@ -51,10 +52,10 @@ public class ThemeUtils {
} else {
resourceId = fallbackDrawable;
}
- return ContextCompat.getDrawable(context, resourceId);
+ return context.getDrawable(resourceId);
}
- public static @DrawableRes int getDrawableId(Context context, @AttrRes int attribute,
+ public static @DrawableRes int getDrawableId(@NonNull Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawableId) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
@@ -64,7 +65,7 @@ public class ThemeUtils {
}
}
- public static @ColorInt int getColor(Context context, @AttrRes int attribute) {
+ public static @ColorInt int getColor(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
if (context.getTheme().resolveAttribute(attribute, value, true)) {
return value.data;
@@ -73,13 +74,13 @@ public class ThemeUtils {
}
}
- public static @ColorRes int getColorId(Context context, @AttrRes int attribute) {
+ public static @ColorRes int getColorId(@NonNull Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(attribute, value, true);
return value.resourceId;
}
- public static @ColorInt int getColorById(Context context, String name) {
+ public static @ColorInt int getColorById(@NonNull Context context, String name) {
return getColor(context,
ResourcesUtils.getResourceIdentifier(context, "attr", name));
}
@@ -88,6 +89,16 @@ public class ThemeUtils {
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN);
}
+ /** this can be replaced with drawableTint in xml once minSdkVersion >= 23 */
+ public static @Nullable Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) {
+ Drawable drawable = context.getDrawable(drawableId);
+ if(drawable == null) {
+ return null;
+ }
+ setDrawableTint(context, drawable, colorAttr);
+ return drawable;
+ }
+
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt b/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt
new file mode 100644
index 000000000..b003cb2d5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/getErrorMessage.kt
@@ -0,0 +1,23 @@
+package com.keylesspalace.tusky.util
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String {
+ return PagingRequestHelper.RequestType.values().mapNotNull {
+ report.getErrorFor(it)?.message
+ }.first()
+}
+
+fun PagingRequestHelper.createStatusLiveData(): LiveData>,
+ it: PagingRequestHelper.Request.Callback) {
+ ioExecutor.execute {
+ handleResponse(accountId, response.body())
+ it.recordSuccess()
+ }
+ }
+
+ override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) {
+ // ignored, since we only ever append to what's in the DB
+ }
+
+ private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback
> {
+ return object : Callback
> {
+ override fun onFailure(call: Call
>, t: Throwable) {
+ it.recordFailure(t)
+ }
+
+ override fun onResponse(call: Call
>, response: Response
>) {
+ insertItemsIntoDb(response, it)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
new file mode 100644
index 000000000..040e29363
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
@@ -0,0 +1,189 @@
+/* Copyright 2019 Conny Duck
+ *
+ * This file is a part of Tusky.
+ *
+ * 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.
+ *
+ * Tusky 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 Tusky; if not,
+ * see
> {
+ override fun onFailure(call: Call
>, t: Throwable) {
+ // retrofit calls this on main thread so safe to call set value
+ networkState.value = NetworkState.error(t.message)
+ }
+
+ override fun onResponse(call: Call
>, response: Response
>) {
+ ioExecutor.execute {
+ db.runInTransaction {
+ db.conversationDao().deleteForAccount(accountId)
+ insertResultIntoDb(accountId, response.body())
+ }
+ // since we are in bg thread now, post the result.
+ networkState.postValue(NetworkState.LOADED)
+ }
+ }
+ }
+ )
+ return networkState
+ }
+
+ @MainThread
+ fun conversations(accountId: Long): Listing
>() {}.type)
+ }
+
+ @TypeConverter
+ fun attachmentListToJson(attachmentList: List
>() {}.type)
+ }
+
+ @TypeConverter
+ fun mentionArrayToJson(mentionArray: Array
> {
override fun onFailure(call: Call
>?, t: Throwable?) {
fetchingStatus = FetchingStatus.NOT_FETCHING
- if (isAdded) {
- swipe_refresh_layout.isRefreshing = false
- progress_bar.visibility = View.GONE
+
+ if(isAdded) {
+ swipeRefreshLayout.isRefreshing = false
+ progressBar.visibility = View.GONE
statusView.show()
if (t is IOException) {
statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
@@ -101,9 +102,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
override fun onResponse(call: Call
>, response: Response
>) {
fetchingStatus = FetchingStatus.NOT_FETCHING
- if (isAdded) {
- swipe_refresh_layout.isRefreshing = false
- progress_bar.visibility = View.GONE
+ if(isAdded) {
+ swipeRefreshLayout.isRefreshing = false
+ progressBar.visibility = View.GONE
val body = response.body()
body?.let { fetched ->
@@ -114,6 +115,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
result.addAll(AttachmentViewData.list(status))
}
adapter.addTop(result)
+
if (statuses.isEmpty()) {
statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
@@ -159,19 +161,19 @@ class AccountMediaFragment : BaseFragment(), Injectable {
super.onViewCreated(view, savedInstanceState)
- val columnCount = context?.resources?.getInteger(R.integer.profile_media_column_count) ?: 2
- val layoutManager = GridLayoutManager(context, columnCount)
+ val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
+ val layoutManager = GridLayoutManager(view.context, columnCount)
- val bgRes = ThemeUtils.getColorId(context, R.attr.window_background)
+ val bgRes = ThemeUtils.getColorId(view.context, R.attr.window_background)
- adapter.baseItemColor = ContextCompat.getColor(recycler_view.context, bgRes)
+ adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes)
- recycler_view.layoutManager = layoutManager
- recycler_view.adapter = adapter
+ recyclerView.layoutManager = layoutManager
+ recyclerView.adapter = adapter
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
- swipe_refresh_layout.setOnRefreshListener {
+ swipeRefreshLayout.setOnRefreshListener {
statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener
currentCall = if (statuses.isEmpty()) {
@@ -184,12 +186,12 @@ class AccountMediaFragment : BaseFragment(), Injectable {
currentCall?.enqueue(callback)
}
- swipe_refresh_layout.setColorSchemeResources(R.color.tusky_blue)
- swipe_refresh_layout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
+ swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
+ swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
statusView.visibility = View.GONE
- recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
index 62a252b52..185f9dede 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
@@ -176,9 +176,9 @@ public class NotificationsFragment extends SFragment implements
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
// Setup the SwipeRefreshLayout.
- swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
- recyclerView = rootView.findViewById(R.id.recycler_view);
- progressBar = rootView.findViewById(R.id.progress_bar);
+ swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
+ recyclerView = rootView.findViewById(R.id.recyclerView);
+ progressBar = rootView.findViewById(R.id.progressBar);
statusView = rootView.findViewById(R.id.statusView);
swipeRefreshLayout.setOnRefreshListener(this);
@@ -417,13 +417,13 @@ public class NotificationsFragment extends SFragment implements
}
@Override
- public void onMore(View view, int position) {
+ public void onMore(@NonNull View view, int position) {
Notification notification = notifications.get(position).asRight();
super.more(notification.getStatus(), view, position);
}
@Override
- public void onViewMedia(int position, int attachmentIndex, View view) {
+ public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
Notification notification = notifications.get(position).asRightOrNull();
if (notification == null || notification.getStatus() == null) return;
super.viewMedia(attachmentIndex, notification.getStatus(), view);
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
index f14abd737..a523a9f14 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -98,7 +98,7 @@ public abstract class SFragment extends BaseFragment {
}
protected void viewThread(Status status) {
- bottomSheetActivity.viewThread(status);
+ bottomSheetActivity.viewThread(status.getActionableId(), status.getUrl());
}
protected void viewAccount(String accountId) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt
index 4250adfb1..d6d6a21c2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SearchFragment.kt
@@ -182,14 +182,14 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
}
}
- override fun onMore(view: View?, position: Int) {
+ override fun onMore(view: View, position: Int) {
val status = searchAdapter.getStatusAtPosition(position)
if (status != null) {
more(status, view, position)
}
}
- override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
+ override fun onViewMedia(position: Int, attachmentIndex: Int, view: View) {
val status = searchAdapter.getStatusAtPosition(position) ?: return
viewMedia(attachmentIndex, status, view)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
index 7b1da9c98..a5214c800 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
@@ -219,9 +219,9 @@ public class TimelineFragment extends SFragment implements
Bundle savedInstanceState) {
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
- recyclerView = rootView.findViewById(R.id.recycler_view);
- swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
- progressBar = rootView.findViewById(R.id.progress_bar);
+ recyclerView = rootView.findViewById(R.id.recyclerView);
+ swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
+ progressBar = rootView.findViewById(R.id.progressBar);
statusView = rootView.findViewById(R.id.statusView);
setupSwipeRefreshLayout();
@@ -608,7 +608,7 @@ public class TimelineFragment extends SFragment implements
}
@Override
- public void onMore(View view, final int position) {
+ public void onMore(@NonNull View view, final int position) {
super.more(statuses.get(position).asRight(), view, position);
}
@@ -689,7 +689,7 @@ public class TimelineFragment extends SFragment implements
}
@Override
- public void onViewMedia(int position, int attachmentIndex, View view) {
+ public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
Status status = statuses.get(position).asRightOrNull();
if (status == null) return;
super.viewMedia(attachmentIndex, status, view);
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
index 399e445d5..865f266b6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
@@ -137,13 +137,13 @@ public final class ViewThreadFragment extends SFragment implements
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false);
Context context = getContext();
- swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
+ swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
swipeRefreshLayout.setOnRefreshListener(this);
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(context, android.R.attr.colorBackground));
- recyclerView = rootView.findViewById(R.id.recycler_view);
+ recyclerView = rootView.findViewById(R.id.recyclerView);
recyclerView.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
@@ -284,12 +284,12 @@ public final class ViewThreadFragment extends SFragment implements
}
@Override
- public void onMore(View view, int position) {
+ public void onMore(@NonNull View view, int position) {
super.more(statuses.get(position), view, position);
}
@Override
- public void onViewMedia(int position, int attachmentIndex, View view) {
+ public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
Status status = statuses.get(position);
super.viewMedia(attachmentIndex, status, view);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
index c97873fd6..7ce075ae6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/AccountPreferencesFragment.kt
@@ -26,10 +26,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import android.util.Log
import android.view.View
-import com.keylesspalace.tusky.AccountListActivity
-import com.keylesspalace.tusky.BuildConfig
-import com.keylesspalace.tusky.PreferencesActivity
-import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.db.AccountManager
@@ -60,6 +57,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
lateinit var eventHub: EventHub
private lateinit var notificationPreference: Preference
+ private lateinit var tabPreference: Preference
private lateinit var mutedUsersPreference: Preference
private lateinit var blockedUsersPreference: Preference
@@ -74,6 +72,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
addPreferencesFromResource(R.xml.account_preferences)
notificationPreference = findPreference("notificationPreference")
+ tabPreference = findPreference("tabPreference")
mutedUsersPreference = findPreference("mutedUsersPreference")
blockedUsersPreference = findPreference("blockedUsersPreference")
defaultPostPrivacyPreference = findPreference("defaultPostPrivacy") as ListPreference
@@ -81,11 +80,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") as SwitchPreference
alwaysShowSensitiveMediaPreference = findPreference("alwaysShowSensitiveMedia") as SwitchPreference
- notificationPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
+ notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
- blockedUsersPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
+ blockedUsersPreference.icon = IconicsDrawable(blockedUsersPreference.context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(blockedUsersPreference.context, R.attr.toolbar_icon_tint))
notificationPreference.onPreferenceClickListener = this
+ tabPreference.onPreferenceClickListener = this
mutedUsersPreference.onPreferenceClickListener = this
blockedUsersPreference.onPreferenceClickListener = this
@@ -161,6 +161,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
}
return true
}
+ tabPreference -> {
+ val intent = Intent(context, TabPreferenceActivity::class.java)
+ activity?.startActivity(intent)
+ activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
+ return true
+ }
mutedUsersPreference -> {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.MUTES)
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
index a6b19bf00..f3aa5bce0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/PreferencesFragment.kt
@@ -34,13 +34,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
addPreferencesFromResource(R.xml.preferences)
val themePreference: Preference = findPreference("appTheme")
- themePreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
+ themePreference.icon = IconicsDrawable(themePreference.context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(themePreference.context, R.attr.toolbar_icon_tint))
val emojiPreference: Preference = findPreference("emojiCompat")
- emojiPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
+ emojiPreference.icon = IconicsDrawable(emojiPreference.context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(emojiPreference.context, R.attr.toolbar_icon_tint))
val textSizePreference: Preference = findPreference("statusTextSize")
- textSizePreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
+ textSizePreference.icon = IconicsDrawable(textSizePreference.context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(textSizePreference.context, R.attr.toolbar_icon_tint))
val timelineFilterPreferences: Preference = findPreference("timelineFilterPreferences")
timelineFilterPreferences.setOnPreferenceClickListener {
diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
index 1ae56d464..31e6230f8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
+++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java
@@ -17,12 +17,14 @@ package com.keylesspalace.tusky.interfaces;
import android.view.View;
+import androidx.annotation.NonNull;
+
public interface StatusActionListener extends LinkListener {
void onReply(int position);
void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, final int position);
- void onMore(View view, final int position);
- void onViewMedia(int position, int attachmentIndex, View view);
+ void onMore(@NonNull View view, final int position);
+ void onViewMedia(int position, int attachmentIndex, @NonNull View view);
void onViewThread(int position);
void onOpenReblog(int position);
void onExpandedChange(boolean expanded, int position);
diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java
index c096caaa5..cabd3eb86 100644
--- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.java
@@ -22,11 +22,14 @@ import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
import com.keylesspalace.tusky.util.HtmlUtils;
import java.lang.reflect.Type;
-public class SpannedTypeAdapter implements JsonDeserializer
> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt
new file mode 100644
index 000000000..fef20bf25
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt
@@ -0,0 +1,46 @@
+/* Copyright 2017 Andrew Dawson
+ *
+ * This file is a part of Tusky.
+ *
+ * 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.
+ *
+ * Tusky 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 Tusky; if not,
+ * see
+ * class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
+ * // TODO replace with an executor from your application
+ * Executor executor = Executors.newSingleThreadExecutor();
+ * PagingRequestHelper helper = new PagingRequestHelper(executor);
+ * // imaginary API service, using Retrofit
+ * MyApi api;
+ *
+ * {@literal @}Override
+ * public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
+ * helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
+ * helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
+ * new Callback<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<ApiResponse> call, Throwable t) {
+ * helperCallback.recordFailure(t);
+ * }
+ * }));
+ * }
+ *
+ * {@literal @}Override
+ * public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
+ * helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
+ * helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
+ * new Callback<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<ApiResponse> call, Throwable t) {
+ * helperCallback.recordFailure(t);
+ * }
+ * }));
+ * }
+ * }
+ *
+ *
+ * MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
+ * helper.addListener(status -> {
+ * // merge multiple states per request type into one, or dispatch separately depending on
+ * // your application logic.
+ * if (status.hasRunning()) {
+ * combined.postValue(PagingRequestHelper.Status.RUNNING);
+ * } else if (status.hasError()) {
+ * // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
+ * combined.postValue(PagingRequestHelper.Status.FAILED);
+ * } else {
+ * combined.postValue(PagingRequestHelper.Status.SUCCESS);
+ * }
+ * });
+ *
+ */
+// THIS class is likely to be moved into the library in a future release. Feel free to copy it
+// from this sample.
+public class PagingRequestHelper {
+ private final Object mLock = new Object();
+ private final Executor mRetryService;
+ @GuardedBy("mLock")
+ private final RequestQueue[] mRequestQueues = new RequestQueue[]
+ {new RequestQueue(RequestType.INITIAL),
+ new RequestQueue(RequestType.BEFORE),
+ new RequestQueue(RequestType.AFTER)};
+ @NonNull
+ final CopyOnWriteArrayList