Skip to content
Snippets Groups Projects
Commit 1211858e authored by Taras's avatar Taras
Browse files

Merge branch 'feature/groups' into develop

parents 49fa19bd 839bbbb6
No related branches found
No related tags found
No related merge requests found
Showing
with 340 additions and 8 deletions
...@@ -29,7 +29,7 @@ android { ...@@ -29,7 +29,7 @@ android {
dimension flavor_dimension_name dimension flavor_dimension_name
applicationIdSuffix ".stage" applicationIdSuffix ".stage"
resValue "string", "app_name", "Circles Stage" resValue "string", "app_name", "Circles Stage"
buildConfigField("String", "MATRIX_HOME_SERVER_URL", "\"https://matrix.kombucha.social/\"") buildConfigField("String", "MATRIX_HOME_SERVER_URL", "\"https://matrix.eu.kombucha.social/\"")
} }
prod { prod {
dimension flavor_dimension_name dimension flavor_dimension_name
...@@ -84,12 +84,15 @@ dependencies { ...@@ -84,12 +84,15 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.nav_version" implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.nav_version"
//ViewBinding //ViewBinding
implementation 'com.github.kirich1409:viewbindingpropertydelegate:1.5.6' implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
//Matrix //Matrix
implementation 'org.matrix.android:matrix-android-sdk2:1.3.18' implementation 'org.matrix.android:matrix-android-sdk2:1.3.18'
implementation 'com.squareup.okhttp3:okhttp:4.9.3' implementation 'com.squareup.okhttp3:okhttp:4.9.3'
//Picasso
implementation 'com.squareup.picasso:picasso:2.71828'
//test //test
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
......
package com.futo.circles.base
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class BaseRecyclerViewHolder<T, VB : ViewBinding> : RecyclerView.ViewHolder {
protected val binding: VB
protected constructor(
parent: ViewGroup,
inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB
) : this(inflate.invoke(LayoutInflater.from(parent.context), parent, false))
private constructor(viewBinding: VB) : super(viewBinding.root) {
this.binding = viewBinding
}
abstract fun bind(data: T)
}
val RecyclerView.ViewHolder.context: Context get() = this.itemView.context
abstract class BaseRvAdapter<T, VH : RecyclerView.ViewHolder>(
itemCallback: DiffUtil.ItemCallback<T>
) : ListAdapter<T, VH>(itemCallback) {
@Suppress("UNCHECKED_CAST")
protected fun <D : T> getItemAs(position: Int): D = getItem(position) as D
companion object {
@Suppress("FunctionName")
@SuppressLint("DiffUtilEquals")
fun <T> DefaultDiffUtilCallback() = object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
}
}
}
package com.futo.circles.di package com.futo.circles.di
import com.futo.circles.ui.groups.GroupsViewModel
import com.futo.circles.ui.groups.timeline.GroupTimelineViewModel
import com.futo.circles.ui.log_in.LogInViewModel import com.futo.circles.ui.log_in.LogInViewModel
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val uiModule = module { val uiModule = module {
viewModel { LogInViewModel(get()) } viewModel { LogInViewModel(get()) }
viewModel { GroupsViewModel(get()) }
viewModel { (roomId: String) -> GroupTimelineViewModel(roomId, get()) }
} }
\ No newline at end of file
...@@ -6,6 +6,8 @@ import android.view.Gravity ...@@ -6,6 +6,8 @@ import android.view.Gravity
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.futo.circles.R import com.futo.circles.R
...@@ -37,14 +39,22 @@ fun Fragment.showError(message: String) { ...@@ -37,14 +39,22 @@ fun Fragment.showError(message: String) {
fun Fragment.setEnabledViews(enabled: Boolean) { fun Fragment.setEnabledViews(enabled: Boolean) {
(view?.rootView as? ViewGroup)?.children?.forEach { (view?.rootView as? ViewGroup)?.children?.forEach {
if(it.isClickable) it.isEnabled = enabled if (it.isClickable) it.isEnabled = enabled
(it as? ViewGroup)?.setEnabledChildren(enabled) (it as? ViewGroup)?.setEnabledChildren(enabled)
} }
} }
fun ViewGroup.setEnabledChildren(enabled: Boolean) { fun ViewGroup.setEnabledChildren(enabled: Boolean) {
children.forEach { children.forEach {
if(it.isClickable) it.isEnabled = enabled if (it.isClickable) it.isEnabled = enabled
(it as? ViewGroup)?.setEnabledChildren(enabled) (it as? ViewGroup)?.setEnabledChildren(enabled)
} }
}
fun Fragment.setSupportActionBar(toolbar: Toolbar) {
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
}
fun Fragment.setToolbarTitle(title: String) {
(activity as? AppCompatActivity)?.supportActionBar?.title = title
} }
\ No newline at end of file
package com.futo.circles.extensions
import android.widget.ImageView
import com.squareup.picasso.Picasso
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
private const val THUMBNAIL_SIZE = 250
fun ImageView.loadMatrixThumbnail(
avatarUrl: String?,
resolver: ContentUrlResolver?,
size: Int = THUMBNAIL_SIZE
) {
val resolvedUrl = resolver?.resolveThumbnail(
avatarUrl,
size, size,
ContentUrlResolver.ThumbnailMethod.SCALE
)
Picasso.get().load(resolvedUrl).into(this)
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ package com.futo.circles.extensions ...@@ -2,6 +2,7 @@ package com.futo.circles.extensions
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
fun <T> LiveData<Response<T>>.observeResponse( fun <T> LiveData<Response<T>>.observeResponse(
...@@ -21,6 +22,13 @@ fun <T> LiveData<Response<T>>.observeResponse( ...@@ -21,6 +22,13 @@ fun <T> LiveData<Response<T>>.observeResponse(
} }
} }
fun <T> LiveData<T>.observeData(fragment: Fragment, observer: (T) -> Unit) {
val owner = fragment.viewLifecycleOwner
observe(owner, Observer {
if (it != null) observer(it)
})
}
suspend fun <T> createResult(block: suspend () -> T): Response<T> { suspend fun <T> createResult(block: suspend () -> T): Response<T> {
return try { return try {
Response.Success(block()) Response.Success(block())
......
package com.futo.circles.extensions
import org.matrix.android.sdk.api.session.room.model.RoomSummary
fun List<RoomSummary>.containsTag(tagName: String) = filter { room ->
room.tags.firstOrNull { tag -> tag.name.contains(tagName) }?.let { true } ?: false
}
fun RoomSummary.nameOrId() = displayName.takeIf { it.isNotEmpty() } ?: roomId
\ No newline at end of file
package com.futo.circles.extensions
import android.view.View
import androidx.recyclerview.widget.RecyclerView
fun RecyclerView.ViewHolder.onClick(view: View, perform: (adapterPosition: Int) -> Unit) =
view.setOnClickListener { bindingAdapterPosition.takeIf { it != -1 }?.let(perform) }
\ No newline at end of file
...@@ -10,10 +10,11 @@ import androidx.navigation.ui.setupWithNavController ...@@ -10,10 +10,11 @@ import androidx.navigation.ui.setupWithNavController
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
import com.futo.circles.R import com.futo.circles.R
import com.futo.circles.databinding.BottomNavigationFragmentBinding import com.futo.circles.databinding.BottomNavigationFragmentBinding
import com.futo.circles.extensions.setSupportActionBar
class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) { class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) {
private val binding: BottomNavigationFragmentBinding by viewBinding() private val binding by viewBinding(BottomNavigationFragmentBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -22,13 +23,14 @@ class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) { ...@@ -22,13 +23,14 @@ class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) {
binding.bottomNavigationView.setupWithNavController(controller) binding.bottomNavigationView.setupWithNavController(controller)
setupToolBar(controller) setupToolBar(controller)
} }
} }
private fun findChildNavController() = private fun findChildNavController() =
(childFragmentManager.findFragmentById(R.id.bottom_nav_host_fragment) as? NavHostFragment)?.navController (childFragmentManager.findFragmentById(R.id.bottom_nav_host_fragment) as? NavHostFragment)?.navController
private fun setupToolBar(navController: NavController) { private fun setupToolBar(navController: NavController) {
setSupportActionBar(binding.toolbar)
val appBarConfiguration = AppBarConfiguration( val appBarConfiguration = AppBarConfiguration(
setOf( setOf(
R.id.homeFragment, R.id.homeFragment,
......
package com.futo.circles.ui.groups package com.futo.circles.ui.groups
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import by.kirich1409.viewbindingdelegate.viewBinding
import com.futo.circles.R import com.futo.circles.R
import com.futo.circles.databinding.GroupsFragmentBinding
import com.futo.circles.extensions.observeData
import com.futo.circles.provider.MatrixSessionProvider
import com.futo.circles.ui.groups.list.GroupsListAdapter
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class GroupsFragment: Fragment(R.layout.groups_fragment)
\ No newline at end of file class GroupsFragment : Fragment(R.layout.groups_fragment) {
private val viewModel by viewModel<GroupsViewModel>()
private val binding by viewBinding(GroupsFragmentBinding::bind)
private val listAdapter by lazy {
GroupsListAdapter(
get<MatrixSessionProvider>().currentSession?.contentUrlResolver(),
::onGroupListItemClicked
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvGroups.apply {
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
adapter = listAdapter
}
viewModel.groupsLiveData?.observeData(this, ::setGroupsList)
}
private fun setGroupsList(list: List<RoomSummary>) {
listAdapter.submitList(list)
}
private fun onGroupListItemClicked(room: RoomSummary) {
findNavController().navigate(
GroupsFragmentDirections.actionGroupsFragment2ToGroupTimelineFragment(
room.roomId
)
)
}
}
\ No newline at end of file
package com.futo.circles.ui.groups
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import com.futo.circles.extensions.containsTag
import com.futo.circles.provider.MatrixSessionProvider
import com.futo.circles.utils.GROUP_TAG
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
class GroupsViewModel(
matrixSessionProvider: MatrixSessionProvider
) : ViewModel() {
val groupsLiveData =
matrixSessionProvider.currentSession?.getRoomSummariesLive(roomSummaryQueryParams())
?.map { list -> list.containsTag(GROUP_TAG) }
}
package com.futo.circles.ui.groups.list
import android.text.format.DateUtils
import android.view.ViewGroup
import com.futo.circles.R
import com.futo.circles.base.BaseRecyclerViewHolder
import com.futo.circles.base.context
import com.futo.circles.databinding.GroupListItemBinding
import com.futo.circles.extensions.loadMatrixThumbnail
import com.futo.circles.extensions.nameOrId
import com.futo.circles.extensions.onClick
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class GroupViewHolder(
parent: ViewGroup,
private val urlResolver: ContentUrlResolver?,
onGroupClicked: (Int) -> Unit
) : BaseRecyclerViewHolder<RoomSummary, GroupListItemBinding>(
parent,
GroupListItemBinding::inflate
) {
init {
onClick(itemView) { position -> onGroupClicked(position) }
}
override fun bind(data: RoomSummary) {
with(binding) {
ivGroup.loadMatrixThumbnail(data.avatarUrl, urlResolver)
ivLock.setImageResource(if (data.isEncrypted) R.drawable.ic_lock else R.drawable.ic_lock_open)
tvGroupTitle.text = data.nameOrId()
val membersCount = data.joinedMembersCount ?: 0
tvMembers.text = context.resources.getQuantityString(
R.plurals.member_plurals,
membersCount, membersCount
)
tvTopic.text = context.getString(
R.string.topic_formatter,
data.topic.takeIf { it.isNotEmpty() } ?: context.getString(R.string.none)
)
data.latestPreviewableEvent?.root?.originServerTs?.let { time ->
tvUpdateTime.text = context.getString(
R.string.last_updated_formatter, DateUtils.getRelativeTimeSpanString(
time,
System.currentTimeMillis(),
DateUtils.HOUR_IN_MILLIS
)
)
}
}
}
}
\ No newline at end of file
package com.futo.circles.ui.groups.list
import android.view.ViewGroup
import com.futo.circles.base.BaseRvAdapter
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.room.model.RoomSummary
class GroupsListAdapter(
private val urlResolver: ContentUrlResolver?,
private val onGroupClicked: (RoomSummary) -> Unit
) : BaseRvAdapter<RoomSummary, GroupViewHolder>(DefaultDiffUtilCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): GroupViewHolder = GroupViewHolder(
parent = parent,
urlResolver = urlResolver,
onGroupClicked = { position -> getItem(position)?.let { onGroupClicked(it) } }
)
override fun onBindViewHolder(holder: GroupViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
}
\ No newline at end of file
package com.futo.circles.ui.groups.timeline
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import by.kirich1409.viewbindingdelegate.viewBinding
import com.futo.circles.R
import com.futo.circles.databinding.GroupTimelineFragmentBinding
import com.futo.circles.extensions.observeData
import com.futo.circles.extensions.setToolbarTitle
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
class GroupTimelineFragment : Fragment(R.layout.group_timeline_fragment) {
private val args: GroupTimelineFragmentArgs by navArgs()
private val viewModel by viewModel<GroupTimelineViewModel> { parametersOf(args.roomId) }
private val binding by viewBinding(GroupTimelineFragmentBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.titleLiveData.observeData(this) { title -> setToolbarTitle(title) }
}
}
\ No newline at end of file
package com.futo.circles.ui.groups.timeline
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.futo.circles.extensions.nameOrId
import com.futo.circles.provider.MatrixSessionProvider
class GroupTimelineViewModel(
private val roomId: String,
private val matrixSessionProvider: MatrixSessionProvider
) : ViewModel() {
val titleLiveData = MutableLiveData(getRoom()?.roomSummary()?.nameOrId() ?: roomId)
private fun getRoom() = matrixSessionProvider.currentSession?.getRoom(roomId)
}
\ No newline at end of file
...@@ -16,7 +16,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel ...@@ -16,7 +16,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
class LogInFragment : Fragment(R.layout.log_in_fragment) { class LogInFragment : Fragment(R.layout.log_in_fragment) {
private val viewModel by viewModel<LogInViewModel>() private val viewModel by viewModel<LogInViewModel>()
private val binding: LogInFragmentBinding by viewBinding() private val binding by viewBinding(LogInFragmentBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
......
package com.futo.circles.utils
const val GROUP_TAG = "group"
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7.7543,7.3043C9.692,7.3043 11.2629,5.6692 11.2629,3.6522C11.2629,1.6351 9.692,0 7.7543,0C5.8165,0 4.2457,1.6351 4.2457,3.6522C4.2457,5.6692 5.8165,7.3043 7.7543,7.3043ZM16.2457,7.3043C18.1834,7.3043 19.7543,5.6692 19.7543,3.6522C19.7543,1.6351 18.1834,0 16.2457,0C14.308,0 12.7371,1.6351 12.7371,3.6522C12.7371,5.6692 14.308,7.3043 16.2457,7.3043ZM19.7543,20.3478C19.7543,22.3649 18.1835,24 16.2457,24C14.308,24 12.7371,22.3649 12.7371,20.3478C12.7371,18.3308 14.308,16.6957 16.2457,16.6957C18.1835,16.6957 19.7543,18.3308 19.7543,20.3478ZM20.4914,15.6522C22.4291,15.6522 24,14.017 24,12C24,9.983 22.4291,8.3478 20.4914,8.3478C18.5537,8.3478 16.9828,9.983 16.9828,12C16.9828,14.017 18.5537,15.6522 20.4914,15.6522ZM11.2629,20.3478C11.2629,22.3649 9.692,24 7.7543,24C5.8165,24 4.2457,22.3649 4.2457,20.3478C4.2457,18.3308 5.8165,16.6957 7.7543,16.6957C9.692,16.6957 11.2629,18.3308 11.2629,20.3478ZM3.5086,15.6522C5.4464,15.6522 7.0172,14.017 7.0172,12C7.0172,9.983 5.4464,8.3478 3.5086,8.3478C1.5709,8.3478 0,9.983 0,12C0,14.017 1.5709,15.6522 3.5086,15.6522Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/blue"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/red"
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" />
</vector>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment