[streaming] Home timeline streaming
This commit is contained in:
parent
54dfc854d4
commit
9bb15e943d
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
|
@ -25,6 +26,7 @@ import android.preference.PreferenceManager;
|
|||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
@ -118,6 +120,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
private Drawer drawer;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private SharedPreferences defPrefs;
|
||||
|
||||
private int notificationTabPosition;
|
||||
private MainPagerAdapter adapter;
|
||||
|
@ -204,12 +207,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
setupTabs(showNotificationTab);
|
||||
|
||||
defPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
||||
viewPager.setPageMargin(pageMargin);
|
||||
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||
R.drawable.tab_page_margin_dark);
|
||||
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
||||
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean("viewPagerOffScreenLimit", false)) {
|
||||
if (defPrefs.getBoolean("viewPagerOffScreenLimit", false)) {
|
||||
viewPager.setOffscreenPageLimit(9);
|
||||
}
|
||||
|
||||
|
@ -270,6 +275,28 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
startStreaming();
|
||||
}
|
||||
|
||||
private void startStreaming() {
|
||||
if (defPrefs.getBoolean("useHTLStream", false)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
stopStreaming();
|
||||
}
|
||||
|
||||
private void stopStreaming() {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (drawer != null && drawer.isDrawerOpen()) {
|
||||
|
@ -538,6 +565,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
SFragment.flushFilters();
|
||||
accountManager.setActiveAccount(newSelectedId);
|
||||
|
||||
stopStreaming();
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
if (forward != null) {
|
||||
|
|
|
@ -18,3 +18,4 @@ data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
|||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class QuickReplyEvent(val status: Status) : Dispatchable
|
||||
data class StreamUpdateEvent(val status: Status, val first: Boolean) : Dispatchable
|
|
@ -0,0 +1,20 @@
|
|||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class StreamEvent(
|
||||
var event: EventType,
|
||||
var payload: String
|
||||
) {
|
||||
|
||||
enum class EventType(val num: Int) {
|
||||
UNKNOWN(0),
|
||||
@SerializedName("update")
|
||||
UPDATE(1),
|
||||
@SerializedName("notification")
|
||||
NOTIFICATION(2),
|
||||
@SerializedName("delete")
|
||||
DELETE(3);
|
||||
}
|
||||
|
||||
}
|
|
@ -45,7 +45,9 @@ import com.keylesspalace.tusky.appstore.QuickReplyEvent;
|
|||
import com.keylesspalace.tusky.appstore.ReblogEvent;
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Filter;
|
||||
|
@ -71,8 +73,11 @@ import com.keylesspalace.tusky.view.BackgroundMessageView;
|
|||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import net.accelf.yuito.TimelineStreamingListener;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -104,6 +109,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
|||
import kotlin.Unit;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.WebSocket;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
@ -178,6 +186,8 @@ public class TimelineFragment extends SFragment implements
|
|||
private boolean reduceTimelineRoading;
|
||||
private boolean checkMobileNetwork;
|
||||
|
||||
private WebSocket webSocket;
|
||||
|
||||
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
|
||||
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
|
||||
@Override
|
||||
|
@ -263,6 +273,56 @@ public class TimelineFragment extends SFragment implements
|
|||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
startStreaming();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
stopStreaming();
|
||||
}
|
||||
|
||||
private void startStreaming() {
|
||||
if (preferences.getBoolean("useHTLStream", false) && kind == Kind.HOME) {
|
||||
connectWebsocket(buildStreamingUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildStreamingUrl() {
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
if (activeAccount != null) {
|
||||
return "wss://" + activeAccount.getDomain() + "/api/v1/streaming/?" + "stream=user" + "&" + "access_token" + "=" + activeAccount.getAccessToken();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void connectWebsocket(String endpoint) {
|
||||
if (webSocket != null) {
|
||||
stopStreaming();
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(endpoint)
|
||||
.build();
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.build();
|
||||
|
||||
webSocket = client.newWebSocket(request, new TimelineStreamingListener(eventHub));
|
||||
}
|
||||
|
||||
private void stopStreaming() {
|
||||
if (webSocket == null) {
|
||||
return;
|
||||
}
|
||||
webSocket.close(1000, null);
|
||||
webSocket = null;
|
||||
}
|
||||
|
||||
private void sendInitialRequest() {
|
||||
if (this.kind == Kind.HOME) {
|
||||
this.tryCache();
|
||||
|
@ -535,6 +595,10 @@ public class TimelineFragment extends SFragment implements
|
|||
handleStatusComposeEvent(status);
|
||||
} else if (event instanceof PreferenceChangedEvent) {
|
||||
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
|
||||
} else if (event instanceof StreamUpdateEvent) {
|
||||
if (kind == Kind.HOME) {
|
||||
handleStreamUpdateEvent((StreamUpdateEvent) event);
|
||||
}
|
||||
}
|
||||
});
|
||||
eventRegistered = true;
|
||||
|
@ -1222,6 +1286,24 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void addStatus(Either<Placeholder, Status> item) {
|
||||
if (item.isRight()) {
|
||||
Status status = item.asRight();
|
||||
if (!((status.getInReplyToId() != null && filterRemoveReplies)
|
||||
|| (status.getReblog() != null && filterRemoveReblogs)
|
||||
|| shouldFilterStatus(status))) {
|
||||
if (findStatusOrReblogPositionById(status.getId()) < 0) {
|
||||
statuses.add(0, item);
|
||||
updateAdapter();
|
||||
timelineRepo.addSingleStatusToDb(status);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
statuses.add(0, item);
|
||||
updateAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For certain requests we don't want to see placeholders, they will be removed some other way
|
||||
*/
|
||||
|
@ -1308,6 +1390,9 @@ public class TimelineFragment extends SFragment implements
|
|||
private void handleStatusComposeEvent(@NonNull Status status) {
|
||||
switch (kind) {
|
||||
case HOME:
|
||||
if (preferences.getBoolean("useHTLStream", false)){
|
||||
return;
|
||||
}
|
||||
case PUBLIC_FEDERATED:
|
||||
case PUBLIC_LOCAL:
|
||||
break;
|
||||
|
@ -1343,6 +1428,16 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
private void handleStreamUpdateEvent(StreamUpdateEvent event) {
|
||||
Status status = event.getStatus();
|
||||
if (event.getFirst() && statuses.get(0).isRight()) {
|
||||
Placeholder placeholder = new Placeholder(statuses.get(0).asRight().getId() + 1);
|
||||
updateStatuses(Arrays.asList(new Either.Right<>(status), new Either.Left<>(placeholder)), false);
|
||||
} else {
|
||||
addStatus(new Either.Right<>(status));
|
||||
}
|
||||
}
|
||||
|
||||
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {
|
||||
return CollectionsKt.map(list, statusLifter);
|
||||
}
|
||||
|
@ -1357,11 +1452,14 @@ public class TimelineFragment extends SFragment implements
|
|||
if (isAdded()) {
|
||||
adapter.notifyItemRangeInserted(position, count);
|
||||
Context context = getContext();
|
||||
if (position == 0 && context != null) {
|
||||
if (isSwipeToRefreshEnabled)
|
||||
if (position == 0 && context != null && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
if (count == 1) {
|
||||
jumpToTop();
|
||||
} else if (isSwipeToRefreshEnabled) {
|
||||
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
|
||||
else
|
||||
} else {
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ enum class TimelineRequestMode {
|
|||
interface TimelineRepository {
|
||||
fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
|
||||
requestMode: TimelineRequestMode): Single<out List<TimelineStatus>>
|
||||
fun addSingleStatusToDb(status: Status)
|
||||
|
||||
companion object {
|
||||
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
|
||||
|
@ -61,6 +62,17 @@ class TimelineRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun addSingleStatusToDb(status: Status) {
|
||||
val acc = accountManager.activeAccount ?: throw IllegalStateException()
|
||||
val accountId = acc.id
|
||||
|
||||
timelineDao.insertInTransaction(
|
||||
status.toEntity(accountId, htmlConverter, gson),
|
||||
status.account.toEntity(accountId, gson),
|
||||
status.reblog?.account?.toEntity(accountId, gson)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStatusesFromNetwork(maxId: String?, sinceId: String?,
|
||||
sinceIdMinusOne: String?, limit: Int,
|
||||
accountId: Long, requestMode: TimelineRequestMode
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package net.accelf.yuito;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StreamEvent;
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
||||
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
public class TimelineStreamingListener extends WebSocketListener {
|
||||
|
||||
private Gson gson = buildGson();
|
||||
private boolean isFirstStatus = true;
|
||||
|
||||
private EventHub eventHub;
|
||||
|
||||
private static Gson buildGson() {
|
||||
return new GsonBuilder()
|
||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
||||
.create();
|
||||
}
|
||||
|
||||
public TimelineStreamingListener(EventHub eventHub) {
|
||||
this.eventHub = eventHub;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.d("StreamingListener", "Stream connected.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
StreamEvent event = gson.fromJson(text, StreamEvent.class);
|
||||
|
||||
String payload = event.getPayload();
|
||||
switch (event.getEvent()) {
|
||||
case UPDATE:
|
||||
Status status = gson.fromJson(payload, Status.class);
|
||||
eventHub.dispatch(new StreamUpdateEvent(status, isFirstStatus));
|
||||
isFirstStatus = false;
|
||||
break;
|
||||
case DELETE:
|
||||
eventHub.dispatch(new StatusDeletedEvent(payload));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
Log.d("StreamingListener", "Stream closed.");
|
||||
}
|
||||
}
|
|
@ -175,6 +175,7 @@
|
|||
<string name="pref_title_limited_bandwidth_mobile">モバイルネットワークでのみ有効</string>
|
||||
<string name="pref_title_limited_bandwidth_timeline">トゥート後のタイムラインの自動更新をしない</string>
|
||||
<string name="pref_title_experimental_viewpager_offscreen">ViewPagerのoffscreenPageLimitを増やす</string>
|
||||
<string name="pref_title_experimental_htl_streaming">HTLのストリーミングを利用する</string>
|
||||
<string name="app_them_dark">ダーク</string>
|
||||
<string name="app_theme_light">ライト</string>
|
||||
<string name="app_theme_black">ブラック</string>
|
||||
|
|
|
@ -223,6 +223,7 @@
|
|||
<string name="pref_title_limited_bandwidth_timeline">Reduce timeline auto reload after toot</string>
|
||||
<string name="pref_title_experimental">Experimental Settings</string>
|
||||
<string name="pref_title_experimental_viewpager_offscreen">Increase offscreenPageLimit of ViewPager</string>
|
||||
<string name="pref_title_experimental_htl_streaming">Use home timeline streaming</string>
|
||||
|
||||
<string name="app_them_dark">Dark</string>
|
||||
<string name="app_theme_light">Light</string>
|
||||
|
|
|
@ -119,5 +119,10 @@
|
|||
android:defaultValue="false"
|
||||
android:key="viewPagerOffScreenLimit"
|
||||
android:title="@string/pref_title_experimental_viewpager_offscreen" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="useHTLStream"
|
||||
android:title="@string/pref_title_experimental_htl_streaming" />
|
||||
</PreferenceCategory>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
|
Loading…
Reference in New Issue