diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 80f9ad943..ec0990fab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -301,14 +301,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje fetchAnnouncements() - streamingManager.setup(lifecycleScope.coroutineContext.job) { active -> - if (active) { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - // Initialise the tab adapter and set to viewpager. Fragments appear to be leaked if the // adapter changes over the life of the viewPager (the adapter, not its contents), so set // the initial list of tabs to empty, and set the full list later in setupTabs(). See @@ -732,15 +724,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState)) } - 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 { - setDrawableTint(this, item.icon!!, android.R.attr.textColorTertiary) - } - } - private fun setupTabs(selectNotificationTab: Boolean) { val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") { val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize) @@ -794,12 +777,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje if (data.id == LIST) { menuBuilder.findItem(R.id.tabEditList).isVisible = true } - if (data.id in arrayOf(HOME, LOCAL, FEDERATED, LIST)) { - menuBuilder.findItem(R.id.tabToggleStreaming).apply { - isVisible = true - isChecked = data.enableStreaming - } - } if (data.id == NOTIFICATIONS) { menuBuilder.findItem(R.id.tabToggleNotificationsFilter).isVisible = true } @@ -813,7 +790,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje setDrawableTint(this, item.icon!!, android.R.attr.textColorPrimary) } } - tintCheckIcon(menuBuilder.findItem(R.id.tabToggleStreaming)) } popup.setOnMenuItemClickListener { item -> @@ -837,20 +813,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje data.arguments.getOrNull(1).orEmpty() ).show(supportFragmentManager, null) } - R.id.tabToggleStreaming -> { - if (fragment is TimelineFragment) { - val to = !item.isChecked - fragment.setStreamingEnabled(to) - item.isChecked = to - tintCheckIcon(item) - - tabs[position] = data.copy(enableStreaming = to) - accountManager.activeAccount?.let { - it.tabPreferences = tabs - accountManager.saveAccount(it) - } - } - } R.id.tabToggleNotificationsFilter -> { if (fragment is NotificationsFragment) { val prefs = PreferenceManager.getDefaultSharedPreferences(this) @@ -915,6 +877,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } updateProfiles() + + streamingManager.setup( + this, + tabs.mapNotNull { it.subscription }.toSet(), + ) { active -> + if (active) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } } private fun refreshComposeButtonState(adapter: MainPagerAdapter, tabPosition: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt index 75aa76b8b..b5276cb65 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -24,6 +24,8 @@ import com.keylesspalace.tusky.components.notifications.NotificationsFragment import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel import com.keylesspalace.tusky.components.trending.TrendingFragment +import net.accelf.yuito.streaming.StreamType +import net.accelf.yuito.streaming.Subscription import java.util.Objects /** this would be a good case for a sealed class, but that does not work nice with Room */ @@ -48,6 +50,20 @@ data class TabData( val title: (Context) -> String = { context -> context.getString(text) }, val enableStreaming: Boolean = false, ) { + val subscription by lazy { + if (enableStreaming) { + when (id) { + HOME -> Subscription(StreamType.USER) + LOCAL -> Subscription(StreamType.LOCAL) + FEDERATED -> Subscription(StreamType.PUBLIC) + LIST -> Subscription(StreamType.LIST, arguments[0].toInt()) + else -> null + } + } else { + null + } + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index cbe86b7cc..a22a2f863 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -212,6 +212,14 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene currentTabsAdapter.notifyItemChanged(tabPosition) } + override fun onStreamingChanged(tab: TabData, tabPosition: Int, enabled: Boolean) { + val newTab = tab.copy(enableStreaming = enabled) + currentTabs[tabPosition] = newTab + saveTabs() + + currentTabsAdapter.notifyItemChanged(tabPosition) + } + private fun toggleFab(expand: Boolean) { val transition = MaterialContainerTransform().apply { startView = if (expand) binding.actionButton else binding.sheet diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index e3e4f27e8..b9b21e9ea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -18,12 +18,16 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.MotionEvent import android.view.ViewGroup +import android.widget.CompoundButton.OnCheckedChangeListener import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import com.google.android.material.chip.Chip +import com.keylesspalace.tusky.FEDERATED import com.keylesspalace.tusky.HASHTAG +import com.keylesspalace.tusky.HOME import com.keylesspalace.tusky.LIST +import com.keylesspalace.tusky.LOCAL import com.keylesspalace.tusky.R import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding @@ -40,6 +44,7 @@ interface ItemInteractionListener { fun onStartDrag(viewHolder: RecyclerView.ViewHolder) fun onActionChipClicked(tab: TabData, tabPosition: Int) fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) + fun onStreamingChanged(tab: TabData, tabPosition: Int, enabled: Boolean) } class TabAdapter( @@ -146,6 +151,18 @@ class TabAdapter( } else { binding.chipGroup.hide() } + + if (tab.id in arrayOf(HOME, LOCAL, FEDERATED, LIST)) { + binding.switchStreaming.show() + + binding.switchStreaming.isChecked = tab.enableStreaming + binding.switchStreaming.setOnCheckedChangeListener { _, isChecked -> + listener.onStreamingChanged(tab, holder.bindingAdapterPosition, isChecked) + binding.switchStreaming.setOnCheckedChangeListener(null) + } + } else { + binding.switchStreaming.hide() + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 5e673bcfd..c9935bd9b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -25,4 +25,4 @@ data class DomainMuteEvent(val instance: String) : Event data class AnnouncementReadEvent(val announcementId: String) : Event data class PinEvent(val statusId: String, val pinned: Boolean) : Event data class QuickReplyEvent(val status: Status) : Event -data class StreamUpdateEvent(val status: Status, val subscription: Subscription) : Event +data class StreamUpdateEvent(val status: Status, val subscription: Subscription, val streamId: Int) : Event diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index 9ef3fd5d8..f22ee735b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -82,7 +82,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import net.accelf.yuito.streaming.StreamingManager import java.io.IOException import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -103,9 +102,6 @@ class TimelineFragment : @Inject lateinit var eventHub: EventHub - @Inject - lateinit var streamingManager: StreamingManager - private val viewModel: TimelineViewModel by unsafeLazy { if (kind == TimelineViewModel.Kind.HOME) { ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java] @@ -180,10 +176,8 @@ class TimelineFragment : kind, id, tags, + arguments.getBoolean(ARG_ENABLE_STREAMING), ) - if (arguments.getBoolean(ARG_ENABLE_STREAMING)) { - setStreamingEnabled(true) - } isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) @@ -428,23 +422,6 @@ class TimelineFragment : binding.recyclerView.adapter = adapter } - override fun onStart() { - super.onStart() - - viewModel.isFirstOfStreaming = true - } - - fun setStreamingEnabled(to: Boolean) { - viewModel.isStreamingEnabled = to - - if (to) { - streamingManager.subscribe(viewModel.subscription) - viewModel.isFirstOfStreaming = true - } else { - streamingManager.unsubscribe(viewModel.subscription) - } - } - override fun onRefresh() { binding.statusView.hide() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 530ec4ffe..88ef3a4ec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -283,15 +283,15 @@ class CachedTimelineViewModel @Inject constructor( // handled by CacheUpdater } - override fun handleStreamUpdateEvent(status: Status) { + override fun handleStreamUpdateEvent(status: Status, streamId: Int) { viewModelScope.launch { val timelineDao = db.timelineDao() val activeAccount = accountManager.activeAccount!! db.withTransaction { - if (isFirstOfStreaming) { + if (streamId != currentStreamId) { timelineDao.insertStatus(Placeholder(status.id, loading = false).toEntity(activeAccount.id)) - isFirstOfStreaming = false + currentStreamId = streamId return@withTransaction } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 3d91aaa1b..fc68149b2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -243,13 +243,13 @@ class NetworkTimelineViewModel @Inject constructor( } } - override fun handleStreamUpdateEvent(status: Status) { + override fun handleStreamUpdateEvent(status: Status, streamId: Int) { viewModelScope.launch { val activeAccount = accountManager.activeAccount!! - if (isFirstOfStreaming) { + if (streamId != currentStreamId) { statusData.add(0, StatusViewData.Placeholder(status.id, isLoading = false)) - isFirstOfStreaming = false + currentStreamId = streamId } else { statusData.add( 0, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index d897766c0..cc01bdcb2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -73,6 +73,7 @@ abstract class TimelineViewModel( private set var tags: List = emptyList() private set + private var isStreamingEnabled = false protected var alwaysShowSensitiveMedia = false private var alwaysOpenSpoilers = false @@ -97,7 +98,7 @@ abstract class TimelineViewModel( } } - var isFirstOfStreaming = false + var currentStreamId: Int = 0 val subscription by lazy { when (kind) { Kind.HOME -> Subscription(StreamType.USER) @@ -115,16 +116,17 @@ abstract class TimelineViewModel( } } } - var isStreamingEnabled = false fun init( kind: Kind, id: String?, tags: List, + isStreamingEnabled: Boolean, ) { this.kind = kind this.id = id this.tags = tags + this.isStreamingEnabled = isStreamingEnabled filterModel.kind = kind.toFilterKind() if (kind == Kind.HOME) { @@ -219,7 +221,7 @@ abstract class TimelineViewModel( abstract fun handlePinEvent(pinEvent: PinEvent) - abstract fun handleStreamUpdateEvent(status: Status) + abstract fun handleStreamUpdateEvent(status: Status, streamId: Int) abstract fun fullReload() @@ -286,7 +288,7 @@ abstract class TimelineViewModel( is PinEvent -> handlePinEvent(event) is StreamUpdateEvent -> { if (isStreamingEnabled && event.subscription == subscription) { - handleStreamUpdateEvent(event.status) + handleStreamUpdateEvent(event.status, event.streamId) } } is MuteConversationEvent -> fullReload() diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 9433c6243..34664ff39 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -56,15 +56,12 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesBaseActivity(): BaseActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesMainActivity(): MainActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesAccountActivity(): AccountActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesListsActivity(): ListsActivity @@ -74,19 +71,15 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesEditProfileActivity(): EditProfileActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesAccountListActivity(): AccountListActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesViewThreadActivity(): ViewThreadActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesStatusListActivity(): StatusListActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesSearchActivity(): SearchActivity @@ -99,7 +92,6 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesPreferencesActivity(): PreferencesActivity @@ -118,11 +110,9 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesReportActivity(): ReportActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesInstanceListActivity(): InstanceListActivity @@ -138,7 +128,6 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesSplashActivity(): SplashActivity - @ActivityScope @ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) abstract fun contributesTrendingActivity(): TrendingActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivityScope.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivityScope.kt deleted file mode 100644 index 0fbc174a9..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivityScope.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.keylesspalace.tusky.di - -import javax.inject.Scope - -@Scope -@Retention(AnnotationRetention.RUNTIME) -annotation class ActivityScope diff --git a/app/src/main/java/net/accelf/yuito/streaming/MastodonStream.kt b/app/src/main/java/net/accelf/yuito/streaming/MastodonStream.kt index 790cfbfee..eb01a2307 100644 --- a/app/src/main/java/net/accelf/yuito/streaming/MastodonStream.kt +++ b/app/src/main/java/net/accelf/yuito/streaming/MastodonStream.kt @@ -10,79 +10,36 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import net.accelf.yuito.streaming.SubscribeRequest.RequestType.SUBSCRIBE -import net.accelf.yuito.streaming.SubscribeRequest.RequestType.UNSUBSCRIBE import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener -import kotlin.coroutines.CoroutineContext class MastodonStream( - parent: Job, + coroutineScope: CoroutineScope, private val okHttpClient: OkHttpClient, private val gson: Gson, private val eventHub: EventHub, - private val onStatusChange: (Boolean) -> Unit, -) : WebSocketListener(), CoroutineScope { +) : WebSocketListener(), CoroutineScope by coroutineScope { private var webSocket: WebSocket? = null - private val subscribing = mutableSetOf() - private val job = SupervisorJob(parent).apply { - invokeOnCompletion { - webSocket?.let { - closeSocket() - } - } - } - override val coroutineContext: CoroutineContext - get() = job - - fun subscribe(subscription: Subscription) { - if (!subscribing.add(subscription)) { - // already subscribed - return - } - - if (webSocket == null) { - openSocket() - } - - send(SubscribeRequest.fromSubscription(SUBSCRIBE, subscription)) - Log.d(TAG, "Subscribed $subscription") - } - - fun unsubscribe(subscription: Subscription) { - if (!subscribing.remove(subscription)) { - // already unsubscribed - return - } - - if (subscribing.isEmpty()) { - closeSocket() - return - } - - send(SubscribeRequest.fromSubscription(UNSUBSCRIBE, subscription)) - Log.d(TAG, "Unsubscribed $subscription") - } - - private fun openSocket() { + fun openSocket(subscriptions: Set) { val request = Request.Builder().url(STREAMING_URL).build() webSocket = okHttpClient.newWebSocket(request, this) - onStatusChange(true) + subscriptions.forEach { + send(SubscribeRequest.fromSubscription(SUBSCRIBE, it)) + Log.d(TAG, "Subscribed $it") + } } - private fun closeSocket() { + fun closeSocket() { webSocket!!.close(1000, null) webSocket = null - onStatusChange(false) } private fun send(subscribeRequest: SubscribeRequest) { @@ -100,7 +57,7 @@ class MastodonStream( StreamEvent.EventType.UPDATE -> { val status = gson.fromJson(payload, Status::class.java) launch { - eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream))) + eventHub.dispatch(StreamUpdateEvent(status, Subscription.fromStreamList(event.stream), this@MastodonStream.hashCode())) } } StreamEvent.EventType.DELETE -> launch { diff --git a/app/src/main/java/net/accelf/yuito/streaming/StreamingManager.kt b/app/src/main/java/net/accelf/yuito/streaming/StreamingManager.kt index b0fc2400d..1483be9f6 100644 --- a/app/src/main/java/net/accelf/yuito/streaming/StreamingManager.kt +++ b/app/src/main/java/net/accelf/yuito/streaming/StreamingManager.kt @@ -1,30 +1,39 @@ package net.accelf.yuito.streaming +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.gson.Gson import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.di.ActivityScope -import kotlinx.coroutines.Job import okhttp3.OkHttpClient import javax.inject.Inject +import javax.inject.Singleton -@ActivityScope +@Singleton class StreamingManager @Inject constructor( private val eventHub: EventHub, private val okHttpClient: OkHttpClient, private val gson: Gson, ) { - private lateinit var stream: MastodonStream + private var stream: MastodonStream? = null - fun setup(parent: Job, onStatusChange: (Boolean) -> Unit) { - stream = MastodonStream(parent, okHttpClient, gson, eventHub, onStatusChange) - } - - fun subscribe(subscription: Subscription) { - stream.subscribe(subscription) - } - - fun unsubscribe(subscription: Subscription) { - stream.unsubscribe(subscription) + fun setup(owner: LifecycleOwner, subscriptions: Set, onStatusChange: (Boolean) -> Unit) { + owner.lifecycle.addObserver(LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + stream = MastodonStream(owner.lifecycleScope, okHttpClient, gson, eventHub) + stream?.openSocket(subscriptions) + onStatusChange(true) + } + Lifecycle.Event.ON_PAUSE -> { + stream?.closeSocket() + stream = null + onStatusChange(false) + } + else -> {} + } + }) } } diff --git a/app/src/main/res/layout/item_tab_preference.xml b/app/src/main/res/layout/item_tab_preference.xml index 9eb164c3e..97bb43191 100644 --- a/app/src/main/res/layout/item_tab_preference.xml +++ b/app/src/main/res/layout/item_tab_preference.xml @@ -59,7 +59,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - app:layout_constraintBottom_toBottomOf="parent"> + app:layout_constraintBottom_toTopOf="@id/switchStreaming"> + + diff --git a/app/src/main/res/menu/view_tab_action.xml b/app/src/main/res/menu/view_tab_action.xml index 14e331bd9..730bfb0c2 100644 --- a/app/src/main/res/menu/view_tab_action.xml +++ b/app/src/main/res/menu/view_tab_action.xml @@ -15,11 +15,6 @@ android:title="@string/action_tab_edit_list" android:visible="false" /> - -