Configure streaming per each tabs

* Can use in Home, Local, Federated
* Process filter_changed event
This commit is contained in:
kyori19 2020-05-22 23:38:40 +09:00
parent 0982834805
commit 831ec88b6f
11 changed files with 285 additions and 181 deletions

View File

@ -22,6 +22,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.net.Uri
@ -63,6 +64,7 @@ import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment
@ -74,10 +76,14 @@ import com.mikepenz.materialdrawer.model.*
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.util.*
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
import com.uber.autodispose.android.lifecycle.autoDispose
import com.uber.autodispose.autoDispose
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import net.accelf.yuito.FooterDrawerItem
import net.accelf.yuito.QuickTootHelper
@ -99,6 +105,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private lateinit var header: AccountHeaderView
private lateinit var drawerToggle: ActionBarDrawerToggle
private var streamingTabsCount = 0
private var notificationTabPosition = 0
private var adapter: MainPagerAdapter? = null
@ -179,7 +186,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
* drawer, though, because its callback touches the header in the drawer. */
fetchUserInfo()
setupTabs(showNotificationTab)
val popups = setupTabs(showNotificationTab)
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
@ -192,66 +199,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
viewPager.offscreenPageLimit = 9
}
val popups = ArrayList<PopupMenu>()
for (i in 0 until tabLayout.tabCount) {
val tab = tabLayout.getTabAt(i)!!
val popup = PopupMenu(this, tab.view)
popup.menuInflater.inflate(R.menu.view_tab_action, popup.menu)
@SuppressLint("RestrictedApi")
if (popup.menu is MenuBuilder) {
val menuBuilder = popup.menu as MenuBuilder
if (tab.position == notificationTabPosition) {
menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true
}
menuBuilder.setOptionalIconsVisible(true)
menuBuilder.visibleItems.forEach { item ->
val iconMarginPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt()
if (item.icon != null) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
item.icon = InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0)
} else {
item.icon = object : InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0) {
override fun getIntrinsicWidth(): Int {
return intrinsicHeight + iconMarginPx + iconMarginPx
}
}
}
ThemeUtils.setDrawableTint(this, item.icon, android.R.attr.textColorPrimary)
}
}
}
popup.setOnMenuItemClickListener { item ->
val fragment = adapter?.getFragment(tab.position)
when (item.itemId) {
R.id.tabJumpToTop -> {
if (fragment is ReselectableFragment) {
(fragment as ReselectableFragment).onReselect()
}
}
R.id.tabReset -> {
if (fragment is ReselectableFragment) {
(fragment as ReselectableFragment).onReset()
}
}
R.id.tabToggleNotificationsFilter -> {
if (fragment is NotificationsFragment) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit().putBoolean("showNotificationsFilter",
!prefs.getBoolean("showNotificationsFilter", true))
.apply()
eventHub.dispatch(PreferenceChangedEvent("showNotificationsFilter"))
}
}
}
false
}
popups.add(popup)
}
tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
if (tab.position == notificationTabPosition) {
@ -294,21 +241,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
override fun onStart() {
super.onStart()
startStreaming()
keepScreenOn()
}
private fun startStreaming() {
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("useHTLStream", false)) {
private fun keepScreenOn() {
if (streamingTabsCount > 0) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
override fun onStop() {
super.onStop()
stopStreaming()
}
private fun stopStreaming() {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
@ -575,12 +520,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
super.onSaveInstanceState(mainDrawer.saveInstanceState(outState))
}
private fun setupTabs(selectNotificationTab: Boolean) {
val tabs = accountManager.activeAccount!!.tabPreferences
private fun tintCheckIcon(item: MenuItem) {
if (item.isChecked) {
@Suppress("DEPRECATION")
item.icon.setColorFilter(ContextCompat.getColor(this, R.color.tusky_green_light), PorterDuff.Mode.SRC_IN)
} else {
ThemeUtils.setDrawableTint(this, item.icon, android.R.attr.textColorTertiary)
}
}
private fun setupTabs(selectNotificationTab: Boolean): ArrayList<PopupMenu> {
val tabs = accountManager.activeAccount!!.tabPreferences.toMutableList()
adapter = MainPagerAdapter(tabs, this)
viewPager.adapter = adapter
TabLayoutMediator(tabLayout, viewPager, TabConfigurationStrategy { _: TabLayout.Tab?, _: Int -> }).attach()
tabLayout.removeAllTabs()
val popups = ArrayList<PopupMenu>()
for (i in tabs.indices) {
val tab = tabLayout.newTab()
.setIcon(tabs[i].icon)
@ -590,13 +545,110 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
tab.setContentDescription(tabs[i].text)
}
tabLayout.addTab(tab)
val popup = PopupMenu(this, tab.view)
popup.menuInflater.inflate(R.menu.view_tab_action, popup.menu)
@SuppressLint("RestrictedApi")
if (popup.menu is MenuBuilder) {
val menuBuilder = popup.menu as MenuBuilder
if (tabs[i].id in arrayOf(HOME, LOCAL, FEDERATED)) {
menuBuilder.findItem(R.id.tabToggleStreaming).apply {
isVisible = true
isChecked = tabs[i].enableStreaming
}
}
if (tabs[i].id == NOTIFICATIONS) {
menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true
}
menuBuilder.setOptionalIconsVisible(true)
menuBuilder.visibleItems.forEach { item ->
val iconMarginPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics).toInt()
if (item.icon != null) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
item.icon = InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0)
} else {
item.icon = object : InsetDrawable(item.icon, iconMarginPx, 0, iconMarginPx, 0) {
override fun getIntrinsicWidth(): Int {
return intrinsicHeight + iconMarginPx + iconMarginPx
}
}
}
ThemeUtils.setDrawableTint(this, item.icon, android.R.attr.textColorPrimary)
}
}
tintCheckIcon(menuBuilder.findItem(R.id.tabToggleStreaming))
}
popup.setOnMenuItemClickListener { item ->
val fragment = adapter?.getFragment(tab.position)
when (item.itemId) {
R.id.tabJumpToTop -> {
if (fragment is ReselectableFragment) {
(fragment as ReselectableFragment).onReselect()
}
}
R.id.tabReset -> {
if (fragment is ReselectableFragment) {
(fragment as ReselectableFragment).onReset()
}
}
R.id.tabToggleStreaming -> {
if (fragment is TimelineFragment) {
fragment.streamingEnabled = !fragment.streamingEnabled
item.isChecked = fragment.streamingEnabled
tintCheckIcon(item)
if (fragment.streamingEnabled) {
streamingTabsCount++
} else {
streamingTabsCount--
}
keepScreenOn()
tabs[i] = tabs[i].copy(enableStreaming = fragment.streamingEnabled)
accountManager.activeAccount?.let {
Single.fromCallable {
it.tabPreferences = tabs
accountManager.saveAccount(it)
}
.subscribeOn(Schedulers.io())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe()
}
}
}
R.id.tabToggleNotificationsFilter -> {
if (fragment is NotificationsFragment) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
prefs.edit().putBoolean("showNotificationsFilter",
!prefs.getBoolean("showNotificationsFilter", true))
.apply()
eventHub.dispatch(PreferenceChangedEvent("showNotificationsFilter"))
}
}
}
false
}
popups.add(popup)
if (tabs[i].id == NOTIFICATIONS) {
notificationTabPosition = i
if (selectNotificationTab) {
tab.select()
}
}
if (tabs[i].enableStreaming) {
streamingTabsCount++
}
}
keepScreenOn()
return popups
}
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
@ -622,7 +674,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
cacheUpdater.stop()
SFragment.flushFilters()
accountManager.setActiveAccount(newSelectedId)
stopStreaming()
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
if (forward != null) {

View File

@ -32,18 +32,22 @@ const val DIRECT = "Direct"
const val HASHTAG = "Hashtag"
const val LIST = "List"
const val STREAMING = "STR"
data class TabData(val id: String,
@StringRes val text: Int,
@DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment,
val arguments: List<String> = emptyList())
val arguments: List<String> = emptyList(),
val enableStreaming: Boolean = false)
fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData {
return when (id) {
HOME -> TabData(HOME, R.string.title_home, R.drawable.ic_home_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) })
val enableStreaming = id.endsWith(STREAMING)
return when (if (enableStreaming) id.slice(IntRange(0, id.length - 4)) else id) {
HOME -> TabData(HOME, R.string.title_home, R.drawable.ic_home_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.HOME, enableStreaming) }, enableStreaming = enableStreaming)
NOTIFICATIONS -> TabData(NOTIFICATIONS, R.string.title_notifications, R.drawable.ic_notifications_24dp, { NotificationsFragment.newInstance() })
LOCAL -> TabData(LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) })
FEDERATED -> TabData(FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) })
LOCAL -> TabData(LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL, enableStreaming) }, enableStreaming = enableStreaming)
FEDERATED -> TabData(FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp, { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED, enableStreaming) }, enableStreaming = enableStreaming)
DIRECT -> TabData(DIRECT, R.string.title_direct_messages, R.drawable.ic_reblog_direct_24dp, { ConversationsFragment.newInstance() })
HASHTAG -> TabData(HASHTAG, R.string.hashtags, R.drawable.ic_hashtag, { args -> TimelineFragment.newHashtagInstance(args) }, arguments)
LIST -> TabData(LIST, R.string.list, R.drawable.ic_list, { args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty()) }, arguments)

View File

@ -4,6 +4,7 @@ import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.fragment.TimelineFragment
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable
@ -21,5 +22,5 @@ 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
data class StreamUpdateEvent(val status: Status, val targetKind: TimelineFragment.Kind, val first: Boolean) : Dispatchable
data class DrawerFooterClickedEvent(val placeholder: Boolean) : Dispatchable

View File

@ -21,6 +21,7 @@ import androidx.core.text.toHtml
import androidx.room.TypeConverter
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.STREAMING
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId
@ -72,7 +73,10 @@ class Converters {
@TypeConverter
fun tabDataToString(tabData: List<TabData>?): String? {
// List name may include ":"
return tabData?.joinToString(";") { it.id + ":" + it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") } }
return tabData?.joinToString(";") {
(if (it.enableStreaming) { it.id + STREAMING } else { it.id }) + ":" +
it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") }
}
}
@TypeConverter

View File

@ -7,14 +7,16 @@ data class StreamEvent(
var payload: String
) {
enum class EventType(val num: Int) {
UNKNOWN(0),
enum class EventType {
UNKNOWN,
@SerializedName("update")
UPDATE(1),
UPDATE,
@SerializedName("notification")
NOTIFICATION(2),
NOTIFICATION,
@SerializedName("delete")
DELETE(3);
DELETE,
@SerializedName("filters_changed")
FILTERS_CHANGED;
}
}
}

View File

@ -133,6 +133,7 @@ public class TimelineFragment extends SFragment implements
private static final String ID_ARG = "id";
private static final String HASHTAGS_ARG = "hastags";
private static final String ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh";
private static final String ARG_ENABLE_STREAMING = "arg.enable.stream";
private static final int LOAD_AT_ONCE = 30;
private boolean isSwipeToRefreshEnabled = true;
@ -193,6 +194,7 @@ public class TimelineFragment extends SFragment implements
private boolean reduceTimelineLoading;
private boolean checkMobileNetwork;
private boolean streamingEnabled;
private WebSocket webSocket;
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
@ -217,16 +219,25 @@ public class TimelineFragment extends SFragment implements
return newInstance(kind, null);
}
public static TimelineFragment newInstance(Kind kind, boolean enableStreaming) {
return newInstance(kind, null, true, enableStreaming);
}
public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId) {
return newInstance(kind, hashtagOrId, true);
}
public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh) {
return newInstance(kind, hashtagOrId, enableSwipeToRefresh, false);
}
public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh, boolean enableStreaming) {
TimelineFragment fragment = new TimelineFragment();
Bundle arguments = new Bundle(3);
arguments.putString(KIND_ARG, kind.name());
arguments.putString(ID_ARG, hashtagOrId);
arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh);
arguments.putBoolean(ARG_ENABLE_STREAMING, enableStreaming);
fragment.setArguments(arguments);
return fragment;
}
@ -272,6 +283,8 @@ public class TimelineFragment extends SFragment implements
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
streamingEnabled = arguments.getBoolean(ARG_ENABLE_STREAMING, false);
}
@Override
@ -306,7 +319,9 @@ public class TimelineFragment extends SFragment implements
@Override
public void onStart() {
super.onStart();
startStreaming();
if (streamingEnabled) {
startStreaming();
}
}
@Override
@ -316,35 +331,41 @@ public class TimelineFragment extends SFragment implements
}
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;
String endpoint = "wss://" + activeAccount.getDomain() + "/api/v1/streaming/"+ "?"
+ "access_token" + "=" + activeAccount.getAccessToken() + "&"
+ "stream" + "=";
switch (kind) {
case HOME: {
endpoint += "user";
break;
}
case PUBLIC_FEDERATED: {
endpoint += "public";
break;
}
case PUBLIC_LOCAL: {
endpoint += "public:local";
break;
}
default: {
return;
}
}
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, kind));
}
}
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;
@ -353,6 +374,19 @@ public class TimelineFragment extends SFragment implements
webSocket = null;
}
public boolean getStreamingEnabled() {
return streamingEnabled;
}
public void setStreamingEnabled(boolean streamingEnabled) {
this.streamingEnabled = streamingEnabled;
if (streamingEnabled) {
startStreaming();
} else {
stopStreaming();
}
}
private void sendInitialRequest() {
if (this.kind == Kind.HOME) {
this.tryCache();
@ -628,7 +662,7 @@ public class TimelineFragment extends SFragment implements
} else if (event instanceof PreferenceChangedEvent) {
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
} else if (event instanceof StreamUpdateEvent) {
if (kind == Kind.HOME) {
if (streamingEnabled) {
handleStreamUpdateEvent((StreamUpdateEvent) event);
}
}
@ -1360,7 +1394,9 @@ public class TimelineFragment extends SFragment implements
if (findStatusOrReblogPositionById(status.getId()) < 0) {
statuses.add(0, item);
updateAdapter();
timelineRepo.addSingleStatusToDb(status);
if (kind == Kind.HOME) {
timelineRepo.addSingleStatusToDb(status);
}
}
}
} else {
@ -1464,11 +1500,12 @@ public class TimelineFragment extends SFragment implements
}
private void handleStatusComposeEvent(@NonNull Status status) {
if (streamingEnabled) {
return;
}
switch (kind) {
case HOME:
if (preferences.getBoolean("useHTLStream", false)){
return;
}
case PUBLIC_FEDERATED:
case PUBLIC_LOCAL:
break;
@ -1505,6 +1542,10 @@ public class TimelineFragment extends SFragment implements
}
private void handleStreamUpdateEvent(StreamUpdateEvent event) {
if (event.getTargetKind() != kind) {
return;
}
Status status = event.getStatus();
if (event.getFirst() && statuses.get(0).isRight()) {
Placeholder placeholder = new Placeholder(statuses.get(0).asRight().getId() + 1);

View File

@ -1,62 +0,0 @@
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.");
}
}

View File

@ -0,0 +1,61 @@
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.PreferenceChangedEvent
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
import com.keylesspalace.tusky.appstore.StreamUpdateEvent
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.StreamEvent
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
class TimelineStreamingListener(private val eventHub: EventHub,
private val kind: TimelineFragment.Kind) : WebSocketListener() {
private val gson = buildGson()
private var isFirstStatus = true
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "Stream connected to: " + kind.name)
}
override fun onMessage(webSocket: WebSocket, text: String) {
val event = gson.fromJson(text, StreamEvent::class.java)
val payload = event.payload
when (event.event) {
StreamEvent.EventType.UPDATE -> {
val status = gson.fromJson(payload, Status::class.java)
eventHub.dispatch(StreamUpdateEvent(status, kind, isFirstStatus))
if (isFirstStatus) {
isFirstStatus = false
}
}
StreamEvent.EventType.DELETE -> eventHub.dispatch(StatusDeletedEvent(payload))
StreamEvent.EventType.FILTERS_CHANGED -> eventHub.dispatch(PreferenceChangedEvent(Filter.HOME)) // It may be not a home but it doesn't matter
else -> Log.d(TAG, "Unsupported event type.")
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "Stream closed for: " + kind.name)
}
companion object {
private const val TAG = "StreamingListener"
private fun buildGson(): Gson {
return GsonBuilder()
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
.create()
}
}
}

View File

@ -9,6 +9,11 @@
android:icon="@drawable/ic_reject_24dp"
android:title="@string/action_tab_reset" />
<item android:id="@+id/tabToggleStreaming"
android:icon="@drawable/ic_check_24dp"
android:title="@string/action_tab_toggle_streaming"
android:visible="false" />
<item android:id="@+id/tabToggleNotificationsFilter"
android:icon="@drawable/ic_notifications_24dp"
android:title="@string/action_tab_toggle_notifications_filter"

View File

@ -144,6 +144,7 @@
<string name="action_authorize">Authorize Now!</string>
<string name="action_tab_jump_to_top">Jump to top</string>
<string name="action_tab_reset">Reset tab</string>
<string name="action_tab_toggle_streaming">Use streaming in this tab</string>
<string name="action_tab_toggle_notifications_filter">Toggle notifications filter</string>
<string name="title_hashtags_dialog">Hashtags</string>

View File

@ -148,11 +148,6 @@
android:title="@string/pref_title_experimental_viewpager_offscreen"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="useHTLStream"
android:title="@string/pref_title_experimental_htl_streaming"
app:singleLineTitle="false" />
</PreferenceCategory>
<PreferenceCategory