Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,6 @@ internal class DashboardFragment : MotionLayoutFragment<
tab.text = dashboardFragmentsAdapter.getPageTitle(position)
}.attach()

viewPagerDashboardTabs.offscreenPageLimit = 3

viewPagerDashboardTabs.post {

viewPagerDashboardTabs.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
Expand All @@ -216,11 +214,20 @@ internal class DashboardFragment : MotionLayoutFragment<
) { }

override fun onPageSelected(position: Int) {
viewModel.updateTabsState(
feedActive = position == DashboardFragmentsAdapter.FEED_TAB_POSITION,
friendsActive = position == DashboardFragmentsAdapter.FRIENDS_TAB_POSITION,
tribesActive = position == DashboardFragmentsAdapter.TRIBES_TAB_POSITION,
)
when (position) {
DashboardFragmentsAdapter.FEED_TAB_POSITION -> viewModel.updateTabsState(
feedActive = true, friendsActive = false, tribesActive = false, workspacesActive = false
)
DashboardFragmentsAdapter.FRIENDS_TAB_POSITION -> viewModel.updateTabsState(
feedActive = false, friendsActive = true, tribesActive = false, workspacesActive = false
)
DashboardFragmentsAdapter.TRIBES_TAB_POSITION -> viewModel.updateTabsState(
feedActive = false, friendsActive = false, tribesActive = true, workspacesActive = false
)
DashboardFragmentsAdapter.WORKSPACES_TAB_POSITION -> viewModel.updateTabsState(
feedActive = false, friendsActive = false, tribesActive = false, workspacesActive = true
)
}
}

override fun onPageScrollStateChanged(state: Int) { }
Expand Down Expand Up @@ -248,6 +255,15 @@ internal class DashboardFragment : MotionLayoutFragment<
val tribesTitle = DashboardFragmentsAdapter.TAB_TITLES[DashboardFragmentsAdapter.TRIBES_TAB_POSITION]
tribesTab?.findViewById<TextView>(R.id.text_view_tab_title)?.text = getString(tribesTitle)

val workspacesTab: View = LayoutInflater.from(this@DashboardFragment.context)
.inflate(R.layout.layout_dashboard_custom_tab, tabs, false)
tabs.getTabAt(DashboardFragmentsAdapter.WORKSPACES_TAB_POSITION)?.customView = workspacesTab

val workspacesTitle = DashboardFragmentsAdapter.TAB_TITLES[DashboardFragmentsAdapter.WORKSPACES_TAB_POSITION]
workspacesTab?.findViewById<TextView>(R.id.text_view_tab_title)?.text = getString(workspacesTitle)

viewPagerDashboardTabs.offscreenPageLimit = 4

if (viewModel.getCurrentPagePosition() != DashboardFragmentsAdapter.FIRST_INIT) {
viewPagerDashboardTabs.currentItem = viewModel.getCurrentPagePosition()
} else
Expand Down Expand Up @@ -731,6 +747,7 @@ internal class DashboardFragment : MotionLayoutFragment<
val feedTab = tabs.getTabAt(DashboardFragmentsAdapter.FEED_TAB_POSITION)?.customView
val friendsTab = tabs.getTabAt(DashboardFragmentsAdapter.FRIENDS_TAB_POSITION)?.customView
val tribesTab = tabs.getTabAt(DashboardFragmentsAdapter.TRIBES_TAB_POSITION)?.customView
val workspacesTab = tabs.getTabAt(DashboardFragmentsAdapter.WORKSPACES_TAB_POSITION)?.customView

feedTab?.findViewById<TextView>(R.id.text_view_tab_title)?.setTextColor(
ContextCompat.getColor(
Expand Down Expand Up @@ -760,6 +777,13 @@ internal class DashboardFragment : MotionLayoutFragment<
tribesTab?.findViewById<View>(R.id.view_unseen_messages_dot)?.goneIfFalse(
viewState.tribesBadgeVisible
)

workspacesTab?.findViewById<TextView>(R.id.text_view_tab_title)?.setTextColor(
ContextCompat.getColor(
binding.root.context,
if (viewState.workspacesActive) R_common.color.text else R_common.color.secondaryText
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package chat.sphinx.dashboard.ui

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import chat.sphinx.dashboard.R
import chat.sphinx.dashboard.ui.feed.FeedFragment
Expand All @@ -19,12 +18,14 @@ class DashboardFragmentsAdapter(
const val FEED_TAB_POSITION = 0
const val FRIENDS_TAB_POSITION = 1
const val TRIBES_TAB_POSITION = 2
const val FIRST_INIT = 3
const val WORKSPACES_TAB_POSITION = 3
const val FIRST_INIT = 4

val TAB_TITLES = arrayOf(
R.string.dashboard_feed_tab_name,
R.string.dashboard_friends_tab_name,
R.string.dashboard_tribes_tab_name,
R.string.dashboard_workspaces_tab_name,
)
}

Expand All @@ -43,6 +44,9 @@ class DashboardFragmentsAdapter(
chatListType = ChatType.Tribe
)
}
WORKSPACES_TAB_POSITION -> {
WorkspacesFragment()
}
else -> {
ChatListFragment.newInstance()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,7 @@ internal class DashboardViewModel @Inject constructor(
feedActive: Boolean? = null,
friendsActive: Boolean? = null,
tribesActive: Boolean? = null,
workspacesActive: Boolean? = null,
friendsBadgeVisible: Boolean? = null,
tribesBadgeVisible: Boolean? = null
) {
Expand All @@ -1353,6 +1354,7 @@ internal class DashboardViewModel @Inject constructor(
feedActive = feedActive ?: currentState.feedActive,
friendsActive = friendsActive ?: currentState.friendsActive,
tribesActive = tribesActive ?: currentState.tribesActive,
workspacesActive = workspacesActive ?: currentState.workspacesActive,
friendsBadgeVisible = friendsBadgeVisible
?: currentState.friendsBadgeVisible,
tribesBadgeVisible = tribesBadgeVisible ?: currentState.tribesBadgeVisible
Expand All @@ -1362,6 +1364,7 @@ internal class DashboardViewModel @Inject constructor(
feedActive = feedActive ?: false,
friendsActive = friendsActive ?: true,
tribesActive = tribesActive ?: false,
workspacesActive = workspacesActive ?: false,
friendsBadgeVisible = friendsBadgeVisible ?: false,
tribesBadgeVisible = tribesBadgeVisible ?: false
)
Expand All @@ -1383,6 +1386,9 @@ internal class DashboardViewModel @Inject constructor(
currentState.tribesActive -> {
DashboardFragmentsAdapter.TRIBES_TAB_POSITION
}
currentState.workspacesActive -> {
DashboardFragmentsAdapter.WORKSPACES_TAB_POSITION
}
else -> DashboardFragmentsAdapter.FRIENDS_TAB_POSITION
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package chat.sphinx.dashboard.ui

import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import by.kirich1409.viewbindingdelegate.viewBinding
import chat.sphinx.concept_image_loader.ImageLoader
import chat.sphinx.dashboard.R
import chat.sphinx.dashboard.databinding.FragmentWorkspacesBinding
import chat.sphinx.dashboard.ui.adapter.WorkspaceAdapter
import dagger.hilt.android.AndroidEntryPoint
import io.matthewnelson.android_feature_screens.ui.sideeffect.SideEffectFragment
import io.matthewnelson.android_feature_screens.util.gone
import io.matthewnelson.android_feature_screens.util.visible
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
internal class WorkspacesFragment : SideEffectFragment<
Context,
ChatListSideEffect,
WorkspacesViewState,
WorkspacesViewModel,
FragmentWorkspacesBinding
>(R.layout.fragment_workspaces) {

@Inject
@Suppress("ProtectedInFinal")
protected lateinit var imageLoader: ImageLoader<ImageView>

override val viewModel: WorkspacesViewModel by viewModels()
override val binding: FragmentWorkspacesBinding by viewBinding(FragmentWorkspacesBinding::bind)

private var workspaceAdapter: WorkspaceAdapter? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
subscribeToWorkspaces()
viewModel.loadWorkspaces()
}

private fun setupRecyclerView() {
workspaceAdapter = WorkspaceAdapter(
imageLoader = imageLoader,
onStopSupervisor = onStopSupervisor,
)

binding.recyclerViewWorkspaces.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = workspaceAdapter
setHasFixedSize(false)
}
}

private fun subscribeToWorkspaces() {
onStopSupervisor.scope.launch(viewModel.mainImmediate) {
viewModel.loading.collect { isLoading ->
if (isLoading) {
binding.progressBarWorkspaces.visible
} else {
binding.progressBarWorkspaces.gone
}
}
}

onStopSupervisor.scope.launch(viewModel.mainImmediate) {
viewModel.workspaces.collect { workspaces ->
workspaceAdapter?.submitList(workspaces)
}
}
}

override suspend fun onViewStateFlowCollect(viewState: WorkspacesViewState) {
// No motion states needed
}

override suspend fun onSideEffectCollect(sideEffect: ChatListSideEffect) {
sideEffect.execute(binding.root.context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package chat.sphinx.dashboard.ui

import androidx.lifecycle.viewModelScope
import chat.sphinx.concept_repository_dashboard.model.Workspace
import chat.sphinx.concept_repository_dashboard_android.RepositoryDashboardAndroid
import dagger.hilt.android.lifecycle.HiltViewModel
import io.matthewnelson.android_feature_viewmodel.SideEffectViewModel
import io.matthewnelson.concept_coroutines.CoroutineDispatchers
import io.matthewnelson.concept_views.viewstate.ViewState
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

internal sealed class WorkspacesViewState : ViewState<WorkspacesViewState>() {
object Idle : WorkspacesViewState()
}

@HiltViewModel
internal class WorkspacesViewModel @Inject constructor(
dispatchers: CoroutineDispatchers,
private val repositoryDashboard: RepositoryDashboardAndroid<Any>,
) : SideEffectViewModel<
android.content.Context,
ChatListSideEffect,
WorkspacesViewState
>(dispatchers, WorkspacesViewState.Idle) {

private val _workspaces = MutableStateFlow<List<Workspace>>(emptyList())
val workspaces: StateFlow<List<Workspace>> = _workspaces.asStateFlow()

private val _loading = MutableStateFlow(false)
val loading: StateFlow<Boolean> = _loading.asStateFlow()

private var fetchJob: Job? = null

fun loadWorkspaces() {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
_loading.value = true
try {
_workspaces.value = repositoryDashboard.fetchWorkspaces()
} catch (e: Exception) {
// fetchWorkspaces() is expected to return emptyList() on failure,
// but guard here so _loading is always reset even on unexpected throws
} finally {
_loading.value = false
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package chat.sphinx.dashboard.ui.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import chat.sphinx.concept_image_loader.Disposable
import chat.sphinx.concept_image_loader.ImageLoader
import chat.sphinx.concept_image_loader.ImageLoaderOptions
import chat.sphinx.concept_repository_dashboard.model.Workspace
import chat.sphinx.dashboard.R
import chat.sphinx.dashboard.databinding.ItemWorkspaceBinding
import io.matthewnelson.android_feature_viewmodel.util.OnStopSupervisor
import kotlinx.coroutines.launch

internal class WorkspaceAdapter(
private val imageLoader: ImageLoader<ImageView>,
private val onStopSupervisor: OnStopSupervisor,
) : ListAdapter<Workspace, WorkspaceAdapter.WorkspaceViewHolder>(DIFF_CALLBACK) {

companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Workspace>() {
override fun areItemsTheSame(oldItem: Workspace, newItem: Workspace): Boolean =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: Workspace, newItem: Workspace): Boolean =
oldItem == newItem
}
}

private val imageLoaderOptions: ImageLoaderOptions = ImageLoaderOptions.Builder()
.placeholderResId(R.drawable.ic_workspace_placeholder)
.build()

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WorkspaceViewHolder {
val binding = ItemWorkspaceBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return WorkspaceViewHolder(binding)
}

override fun onBindViewHolder(holder: WorkspaceViewHolder, position: Int) {
holder.bind(getItem(position))
}

override fun onViewRecycled(holder: WorkspaceViewHolder) {
super.onViewRecycled(holder)
holder.onRecycled()
}

inner class WorkspaceViewHolder(
private val binding: ItemWorkspaceBinding
) : RecyclerView.ViewHolder(binding.root) {

var disposable: Disposable? = null

fun bind(workspace: Workspace) {
binding.apply {
textViewWorkspaceName.text = workspace.name
textViewWorkspaceRole.text = workspace.userRole ?: "Member"
textViewWorkspaceMembers.text = "Members: ${workspace.memberCount}"

disposable?.dispose()
disposable = null

val logoUrl = workspace.logoUrl
if (logoUrl != null) {
onStopSupervisor.scope.launch {
imageLoader.load(
imageViewWorkspaceLogo,
logoUrl,
imageLoaderOptions
).also { disposable = it }
}
} else {
imageViewWorkspaceLogo.setImageResource(R.drawable.ic_workspace_placeholder)
}
}
}

fun onRecycled() {
disposable?.dispose()
disposable = null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sealed class DashboardTabsViewState: ViewState<DashboardTabsViewState>() {
val feedActive: Boolean,
val friendsActive: Boolean,
val tribesActive: Boolean,
val workspacesActive: Boolean = false,
val friendsBadgeVisible: Boolean,
val tribesBadgeVisible: Boolean,
) : DashboardTabsViewState()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#3A3A4A" />
<corners android:radius="8dp" />
</shape>
Loading